From 76364591e1b5ece080742ff310591563e911f75b Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 18:03:55 +0200 Subject: [PATCH 01/19] add common/json --- common/json.cpp | 230 +++++++++++++++++++++++++++++++++++++++++++++++ common/json.h | 234 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 464 insertions(+) create mode 100644 common/json.cpp create mode 100644 common/json.h diff --git a/common/json.cpp b/common/json.cpp new file mode 100644 index 000000000000..aada499fe618 --- /dev/null +++ b/common/json.cpp @@ -0,0 +1,230 @@ +#include "json.h" + +#include "ggml.h" + +#define JSON_ASSERT GGML_ASSERT +#include + +#include +#include + +using nlohmann::ordered_json; + +// common_json_node is never defined, it only stands for an ordered_json in this file +static ordered_json & as_json(common_json_node * node) { + return *reinterpret_cast(node); +} + +static const ordered_json & as_json(const common_json_node * node) { + return *reinterpret_cast(node); +} + +static common_json_node * as_node(ordered_json * json) { + return reinterpret_cast(json); +} + +void common_json_node_deleter::operator()(common_json_node * node) const { + delete reinterpret_cast(node); +} + +static common_json make_json(const ordered_json & val) { + common_json out; + as_json(out.get_node()) = val; + return out; +} + +static ordered_json to_json(const common_json_value & val) { + switch (val.type) { + case common_json_value::VAL_NULL: return nullptr; + case common_json_value::VAL_BOOL: return val.val_bool; + case common_json_value::VAL_INT: return val.val_int; + case common_json_value::VAL_UINT: return val.val_uint; + case common_json_value::VAL_DOUBLE: return val.val_double; + case common_json_value::VAL_STRING: return val.val_string; + case common_json_value::VAL_JSON: return as_json(val.val_json->get_node()); + } + + return nullptr; +} + +template T & common_json_raw(common_json_ref & json) { + return as_json(json.get_node()); +} + +template const T & common_json_raw(const common_json_ref & json) { + return as_json(json.get_node()); +} + +template common_json common_json_from_raw(const T & json) { + return make_json(json); +} + +// the bridge is usable only for the type below +template ordered_json & common_json_raw(common_json_ref &); +template const ordered_json & common_json_raw(const common_json_ref &); +template common_json common_json_from_raw(const ordered_json &); + +common_json_value::common_json_value(const char * val) { + if (val) { + type = VAL_STRING; + val_string = val; + } else { + type = VAL_NULL; + } +} + +common_json_value::common_json_value(const common_json & val) : + type(VAL_JSON), val_json(std::make_shared(val)) {} + +common_json_value::common_json_value(const common_json_ref & val) : + type(VAL_JSON), val_json(std::make_shared(make_json(as_json(val.get_node())))) {} + +bool common_json_ref::is_null() const { return as_json(node).is_null(); } +bool common_json_ref::is_object() const { return as_json(node).is_object(); } +bool common_json_ref::is_array() const { return as_json(node).is_array(); } +bool common_json_ref::is_string() const { return as_json(node).is_string(); } +bool common_json_ref::is_boolean() const { return as_json(node).is_boolean(); } +bool common_json_ref::is_number() const { return as_json(node).is_number(); } +bool common_json_ref::is_number_integer() const { return as_json(node).is_number_integer(); } + +bool common_json_ref::empty() const { return as_json(node).empty(); } +size_t common_json_ref::size() const { return as_json(node).size(); } + +bool common_json_ref::contains(const std::string & key) const { + return as_json(node).contains(key); +} + +bool common_json_ref::operator==(const common_json_value & val) const { + return as_json(node) == to_json(val); +} + +bool common_json_ref::operator!=(const common_json_value & val) const { + return !(*this == val); +} + +common_json_ref common_json_ref::at(const std::string & key) const { + return common_json_ref(as_node(&as_json(node).at(key))); +} + +common_json_ref common_json_ref::operator[](const std::string & key) const { + return common_json_ref(as_node(&as_json(node)[key])); +} + +common_json_ref common_json_ref::operator[](size_t idx) const { + return common_json_ref(as_node(&as_json(node)[idx])); +} + +void common_json_ref::assign(const common_json_value & val) { + as_json(node) = to_json(val); +} + +void common_json_ref::set(const common_json_item & item) { + as_json(node)[item.key] = to_json(item.val); +} + +void common_json_ref::push_back(const common_json_value & val) { + as_json(node).push_back(to_json(val)); +} + +std::string common_json_ref::dump(int indent) const { + return as_json(node).dump(indent); +} + +// an array is indexed directly, an object needs a walk from the start +common_json_ref common_json_ref::iterator::operator*() const { + if (as_json(node).is_object()) { + return common_json_ref(as_node(&std::next(as_json(node).begin(), idx).value())); + } + + return common_json_ref(as_node(&as_json(node)[idx])); +} + +std::string common_json_ref::iterator::key() const { + return std::next(as_json(node).begin(), idx).key(); +} + +std::pair common_json_ref::items_view::iterator::operator*() const { + auto it = std::next(as_json(node).begin(), idx); + + return { it.key(), common_json_ref(as_node(&it.value())) }; +} + +common_json::common_json() : + common_json_ref(nullptr), pimpl(as_node(new ordered_json(ordered_json::object()))) { + node = pimpl.get(); +} + +common_json::common_json(std::initializer_list items) : common_json() { + for (const auto & item : items) { + set(item); + } +} + +common_json::common_json(const common_json & other) : + common_json_ref(nullptr), pimpl(as_node(new ordered_json(as_json(other.node)))) { + node = pimpl.get(); +} + +common_json::common_json(common_json && other) noexcept : + common_json_ref(other.node), pimpl(std::move(other.pimpl)) { + other.node = nullptr; +} + +common_json & common_json::operator=(const common_json & other) { + as_json(node) = as_json(other.node); + + return *this; +} + +common_json & common_json::operator=(common_json && other) noexcept { + pimpl = std::move(other.pimpl); + node = pimpl.get(); + + other.node = nullptr; + + return *this; +} + +common_json::~common_json() = default; + +common_json common_json::parse(const std::string & text) { + try { + return make_json(ordered_json::parse(text)); + } catch (const std::exception & e) { + throw common_json_error(e.what()); + } +} + +common_json common_json::array() { + return make_json(ordered_json::array()); +} + +common_json common_json::object() { + return common_json(); +} + +common_json common_json::make(const common_json_value & val) { + return make_json(to_json(val)); +} + +template T common_json_ref::get() const { + return as_json(node).get(); +} + +// get() is usable only for the types below + +#define COMMON_JSON_GET(...) template __VA_ARGS__ common_json_ref::get<__VA_ARGS__>() const; + +COMMON_JSON_GET(bool) +COMMON_JSON_GET(int) +COMMON_JSON_GET(unsigned int) +COMMON_JSON_GET(long) +COMMON_JSON_GET(unsigned long) +COMMON_JSON_GET(long long) +COMMON_JSON_GET(unsigned long long) +COMMON_JSON_GET(float) +COMMON_JSON_GET(double) +COMMON_JSON_GET(std::string) +COMMON_JSON_GET(std::vector) + +#undef COMMON_JSON_GET diff --git a/common/json.h b/common/json.h new file mode 100644 index 000000000000..ab4163790e24 --- /dev/null +++ b/common/json.h @@ -0,0 +1,234 @@ +#pragma once + +// JSON object, it works without the need to include a JSON library header +// note: object keys keep the order in which they are added + +#include +#include +#include +#include +#include +#include +#include +#include + +class common_json; +class common_json_ref; + +// one value of the backing library, only json.cpp knows what it is +struct common_json_node; + +struct common_json_node_deleter { + void operator()(common_json_node * node) const; +}; + +struct common_json_error : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +// one value, tagged so that this header stays free of the backing library +struct common_json_value { + enum value_type { + VAL_NULL, + VAL_BOOL, + VAL_INT, + VAL_UINT, + VAL_DOUBLE, + VAL_STRING, + VAL_JSON, + }; + + value_type type = VAL_NULL; + + union { + bool val_bool; + int64_t val_int; + uint64_t val_uint = 0; + double val_double; + }; + + std::string val_string; + std::shared_ptr val_json; + + common_json_value(std::nullptr_t = nullptr) : type(VAL_NULL) {} + common_json_value(bool val) : type(VAL_BOOL), val_bool(val) {} + common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} + common_json_value(const char * val); + common_json_value(const common_json & val); + common_json_value(const common_json_ref & val); + + template ::value && !std::is_same::value, int>::type = 0> + common_json_value(T val) : type(std::is_signed::value ? VAL_INT : VAL_UINT) { + if (std::is_signed::value) { + val_int = (int64_t) val; + } else { + val_uint = (uint64_t) val; + } + } + + template ::value, int>::type = 0> + common_json_value(T val) : type(VAL_DOUBLE), val_double((double) val) {} +}; + +struct common_json_item { + std::string key; + common_json_value val; + + template + common_json_item(std::string key, T && val) : + key(std::move(key)), val(std::forward(val)) {} +}; + +// view to a value owned by a common_json, it goes stale if the owner gets a new key +class common_json_ref { + public: + explicit common_json_ref(common_json_node * node) : node(node) {} + + common_json_ref(const common_json_ref &) = default; + + // rebinding a view is almost always a write-through by mistake, use assign() to write + common_json_ref & operator=(const common_json_ref &) = delete; + + bool is_null() const; + bool is_object() const; + bool is_array() const; + bool is_string() const; + bool is_boolean() const; + bool is_number() const; + bool is_number_integer() const; + + bool empty() const; + size_t size() const; + + bool contains(const std::string & key) const; + + bool operator==(const common_json_value & val) const; + bool operator!=(const common_json_value & val) const; + + // at() throws if the key is missing, operator[] adds a null value instead + common_json_ref at(const std::string & key) const; + common_json_ref operator[](const std::string & key) const; + common_json_ref operator[](size_t idx) const; + + // only for the types instantiated in json.cpp, the rest fails at link time + template T get() const; + + template + T value(const std::string & key, T def) const { + return contains(key) ? at(key).get() : def; + } + + std::string value(const std::string & key, const char * def) const { + return contains(key) ? at(key).get() : std::string(def); + } + + void assign(const common_json_value & val); + void set(const common_json_item & item); + void push_back(const common_json_value & val); + + template + common_json_ref & operator=(T && val) { + assign(common_json_value(std::forward(val))); + return *this; + } + + std::string dump(int indent = -1) const; + + // walks an array by index, or an object in insertion order + class iterator { + public: + iterator(common_json_node * node, size_t idx) : node(node), idx(idx) {} + + common_json_ref operator*() const; + std::string key() const; + + iterator & operator++() { + idx++; + return *this; + } + + bool operator!=(const iterator & other) const { return idx != other.idx; } + bool operator==(const iterator & other) const { return idx == other.idx; } + + private: + common_json_node * node; + size_t idx; + }; + + iterator begin() const { return iterator(node, 0); } + iterator end() const { return iterator(node, size()); } + + // allows: for (const auto & [key, val] : obj.items()) + class items_view { + public: + items_view(common_json_node * node, size_t n) : node(node), n(n) {} + + class iterator { + public: + iterator(common_json_node * node, size_t idx) : node(node), idx(idx) {} + + std::pair operator*() const; + + iterator & operator++() { + idx++; + return *this; + } + + bool operator!=(const iterator & other) const { return idx != other.idx; } + + private: + common_json_node * node; + size_t idx; + }; + + iterator begin() const { return iterator(node, 0); } + iterator end() const { return iterator(node, n); } + + private: + common_json_node * node; + size_t n; + }; + + items_view items() const { return items_view(node, size()); } + + common_json_node * get_node() const { return node; } + + protected: + common_json_node * node; +}; + +// owns the value it points to +class common_json : public common_json_ref { + public: + common_json(); + common_json(std::initializer_list items); + common_json(const common_json & other); + common_json(common_json && other) noexcept; + + common_json & operator=(const common_json & other); + common_json & operator=(common_json && other) noexcept; + + // out-of-line, the deleter needs to know the real type + ~common_json(); + + // throws common_json_error if the text is not valid JSON + static common_json parse(const std::string & text); + + static common_json array(); + static common_json object(); + + // holds a single value, e.g. make("abc").dump() gives "\"abc\"" + static common_json make(const common_json_value & val); + + private: + std::unique_ptr pimpl; +}; + +// bridge for code that still uses internal component from nlohmann::json +// usage: common_json_raw(j) +// TODO: maybe completely remove this in the future + +template T & common_json_raw(common_json_ref & json); +template const T & common_json_raw(const common_json_ref & json); + +template common_json common_json_from_raw(const T & json); From 7a3360f6d82216063c9579d66ebb7e628b79c62f Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 18:04:08 +0200 Subject: [PATCH 02/19] migrate common --- common/CMakeLists.txt | 2 ++ common/arg.cpp | 20 +++++++++----------- common/chat-auto-parser-helpers.cpp | 3 --- common/chat.cpp | 2 +- common/download.cpp | 16 ++++++---------- common/hf-cache.cpp | 18 +++++++----------- common/json-schema-to-grammar.cpp | 4 ++-- common/json-schema-to-grammar.h | 4 +++- tools/server/server-schema.cpp | 2 +- 9 files changed, 31 insertions(+), 40 deletions(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 54691da3f41e..36f1e0cd50f1 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -81,6 +81,8 @@ add_library(${TARGET} imatrix-loader.cpp imatrix-loader.h json-schema-to-grammar.cpp + json.cpp + json.h llguidance.cpp log.cpp log.h diff --git a/common/arg.cpp b/common/arg.cpp index 0a479c6aaa8a..4a36ff9b0ccc 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -5,6 +5,7 @@ #include "common.h" #include "download.h" #include "json-schema-to-grammar.h" +#include "json.h" #include "llama.h" #include "log.h" #include "sampling.h" @@ -21,9 +22,6 @@ #include #endif -#define JSON_ASSERT GGML_ASSERT -#include - #include #include #include @@ -32,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -55,7 +54,6 @@ #define LLAMA_MAX_URL_LENGTH 2084 // Maximum URL Length in Chrome: 2083 -using json = nlohmann::ordered_json; using namespace common_arg_utils; static std::initializer_list mmproj_examples = { @@ -2272,7 +2270,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"-j", "--json-schema"}, "SCHEMA", "JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object\nFor schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead", [](common_params & params, const std::string & value) { - params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(value))}; + params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(common_json::parse(value))}; } ).set_sampling()); add_opt(common_arg( @@ -2289,7 +2287,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex std::istreambuf_iterator(), std::back_inserter(schema) ); - params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(schema))}; + params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(common_json::parse(schema))}; } ).set_sampling()); add_opt(common_arg( @@ -3500,13 +3498,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"--chat-template-kwargs"}, "STRING", "sets additional params for the json template parser, must be a valid json object string, e.g. '{\"key1\":\"value1\",\"key2\":\"value2\"}'", [](common_params & params, const std::string & value) { - auto parsed = json::parse(value); - for (const auto & item : parsed.items()) { - if (item.key() == "enable_thinking") { + auto parsed = common_json::parse(value); + for (const auto & [key, val] : parsed.items()) { + if (key == "enable_thinking") { LOG_WRN("Setting 'enable_thinking' via --chat-template-kwargs is deprecated. " "Use --reasoning on / --reasoning off instead.\n"); } - params.default_template_kwargs[item.key()] = item.value().dump(); + params.default_template_kwargs[key] = val.dump(); } } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_CHAT_TEMPLATE_KWARGS")); @@ -3674,7 +3672,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex if (value == "default") { params.default_template_kwargs.erase("reasoning_effort"); } else { - params.default_template_kwargs["reasoning_effort"] = json(value).dump(); + params.default_template_kwargs["reasoning_effort"] = common_json::make(value).dump(); } } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_REASONING_EFFORT")); diff --git a/common/chat-auto-parser-helpers.cpp b/common/chat-auto-parser-helpers.cpp index 81b17e5e1d27..b37906bdf85a 100644 --- a/common/chat-auto-parser-helpers.cpp +++ b/common/chat-auto-parser-helpers.cpp @@ -4,14 +4,11 @@ #include "chat-peg-parser.h" #include "chat.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" #include #include -using json = nlohmann::ordered_json; - std::string trim_whitespace(const std::string & str) { size_t start = 0; while (start < str.length() && std::isspace(static_cast(str[start]))) { diff --git a/common/chat.cpp b/common/chat.cpp index 39761f12acdd..ee2e777af234 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -3799,7 +3799,7 @@ static common_chat_params common_chat_templates_apply_legacy(const struct common common_chat_params params; params.prompt = std::string(buf.data(), res); if (!inputs.json_schema.empty()) { - params.grammar = json_schema_to_grammar(json::parse(inputs.json_schema)); + params.grammar = json_schema_to_grammar(common_json::parse(inputs.json_schema)); } else { params.grammar = inputs.grammar; } diff --git a/common/download.cpp b/common/download.cpp index 2509f75ab996..4b28a708c86e 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -5,9 +5,7 @@ #include "log.h" #include "download.h" #include "hf-cache.h" - -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -44,8 +42,6 @@ #include #endif -using json = nlohmann::ordered_json; - // // downloader // @@ -856,8 +852,8 @@ static std::string common_docker_get_token(const std::string & repo) { throw std::runtime_error("Failed to get Docker registry token, HTTP code: " + std::to_string(res.first)); } - std::string response_str(res.second.begin(), res.second.end()); - nlohmann::ordered_json response = nlohmann::ordered_json::parse(response_str); + std::string response_str(res.second.begin(), res.second.end()); + common_json response = common_json::parse(response_str); if (!response.contains("token")) { throw std::runtime_error("Docker registry token response missing 'token' field"); @@ -919,9 +915,9 @@ std::string common_docker_resolve_model(const std::string & docker) { throw std::runtime_error("Failed to get Docker manifest, HTTP code: " + std::to_string(manifest_res.first)); } - std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end()); - nlohmann::ordered_json manifest = nlohmann::ordered_json::parse(manifest_str); - std::string gguf_digest; // Find the GGUF layer + std::string manifest_str(manifest_res.second.begin(), manifest_res.second.end()); + common_json manifest = common_json::parse(manifest_str); + std::string gguf_digest; // Find the GGUF layer if (manifest.contains("layers")) { for (const auto & layer : manifest["layers"]) { if (layer.contains("mediaType")) { diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index f1dacaa4778d..50d6dd6105c4 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -4,9 +4,7 @@ #include "common.h" #include "log.h" #include "http.h" - -#define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -15,8 +13,6 @@ #include #include -namespace nl = nlohmann; - #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #ifndef NOMINMAX @@ -195,8 +191,8 @@ static void safe_write_file(const fs::path & path, const std::string & data) { } } -static nl::json api_get(const std::string & url, - const std::string & token) { +static common_json api_get(const std::string & url, + const std::string & token) { auto [cli, parts] = common_http_client(url); httplib::Headers headers = { @@ -214,10 +210,10 @@ static nl::json api_get(const std::string & url, auto body = res->body; if (res->status == 200) { - return nl::json::parse(res->body); + return common_json::parse(res->body); } try { - body = nl::json::parse(res->body)["error"].get(); + body = common_json::parse(res->body)["error"].get(); } catch (...) { } throw std::runtime_error("GET failed (" + std::to_string(res->status) + "): " + body); @@ -280,7 +276,7 @@ static std::string get_repo_commit(const std::string & repo_id, safe_write_file(refs_path / name, commit); return commit; - } catch (const nl::json::exception & e) { + } catch (const common_json_error & e) { LOG_ERR("%s: JSON error: %s\n", __func__, e.what()); } catch (const std::exception & e) { LOG_ERR("%s: error: %s\n", __func__, e.what()); @@ -358,7 +354,7 @@ hf_files get_repo_files(const std::string & repo_id, files.push_back(file); } - } catch (const nl::json::exception & e) { + } catch (const common_json_error & e) { LOG_ERR("%s: JSON error: %s\n", __func__, e.what()); } catch (const std::exception & e) { LOG_ERR("%s: error: %s\n", __func__, e.what()); diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 955b4e014b09..e7b12aed8a76 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -1227,7 +1227,7 @@ bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schem return check(schema); } -std::string json_schema_to_grammar(const json & schema, bool force_gbnf) { +std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) { #ifdef LLAMA_USE_LLGUIDANCE if (!force_gbnf) { return "%llguidance {}\nstart: %json " + schema.dump(); @@ -1236,7 +1236,7 @@ std::string json_schema_to_grammar(const json & schema, bool force_gbnf) { (void)force_gbnf; #endif // LLAMA_USE_LLGUIDANCE return build_grammar([&](const common_grammar_builder & callbacks) { - auto copy = schema; + auto copy = common_json_raw(schema); callbacks.resolve_refs(copy); callbacks.add_schema("", copy); }); diff --git a/common/json-schema-to-grammar.h b/common/json-schema-to-grammar.h index 240d64231154..bab5591effcb 100644 --- a/common/json-schema-to-grammar.h +++ b/common/json-schema-to-grammar.h @@ -1,12 +1,14 @@ #pragma once +#include "json.h" + #include #include #include #include -std::string json_schema_to_grammar(const nlohmann::ordered_json & schema, +std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf = false); class common_schema_converter; diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 5d7fa6ae6ec3..33c4ecd46ebf 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -258,7 +258,7 @@ std::vector> make_llama_cmpl_schema(const common_params & try { auto schema = json_value(data, "json_schema", json::object()); SRV_DBG("JSON schema: %s\n", schema.dump(2).c_str()); - std::string grammar_str = json_schema_to_grammar(schema); + std::string grammar_str = json_schema_to_grammar(common_json_from_raw(schema)); SRV_DBG("Converted grammar: %s\n", grammar_str.c_str()); params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, std::move(grammar_str)}; } catch (const std::exception & e) { From f2621800844ac584049ba4bc4ec4888effbcd21f Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 18:51:09 +0200 Subject: [PATCH 03/19] adapt jinja --- common/chat.cpp | 3 ++- common/jinja/README.md | 2 +- common/jinja/caps.cpp | 6 +++--- common/jinja/value.cpp | 22 ++++++++++++---------- common/jinja/value.h | 2 +- common/json.cpp | 23 +++++++++++++++++++---- common/json.h | 22 +++++++++++++++++++--- 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index ee2e777af234..0461c00234b1 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -6,6 +6,7 @@ #include "common.h" #include "ggml.h" #include "json-schema-to-grammar.h" +#include "json.h" #include "log.h" #include "jinja/value.h" @@ -972,7 +973,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } - jinja::global_from_json(ctx, inp, inputs.mark_input); + jinja::global_from_json(ctx, common_json_ref_from_raw(inp), inputs.mark_input); // render jinja::runtime runtime(ctx); diff --git a/common/jinja/README.md b/common/jinja/README.md index 8291240767e8..5b97fc92c5a7 100644 --- a/common/jinja/README.md +++ b/common/jinja/README.md @@ -7,7 +7,7 @@ The implementation can be found in the `common/jinja` directory. ## Key Features - Input marking: security against special token injection -- Decoupled from `nlohmann::json`: this dependency is only used for JSON-to-internal type translation and is completely optional +- Decoupled from the JSON library: `common_json` is only used for JSON-to-internal type translation and is completely optional - Minimal primitive types: int, float, bool, string, array, object, none, undefined - Detailed logging: allow source tracing on error - Clean architecture: workarounds are applied to input data before entering the runtime (see `common/chat.cpp`) diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 6e3a1e9b298a..53965d990335 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -4,14 +4,14 @@ // note: the json dependency is only for defining input in a convenient way // we can remove it in the future when we figure out a better way to define inputs using jinja::value -#include +#include "json.h" #include #include #define FILENAME "jinja-caps" -using json = nlohmann::ordered_json; +using json = common_json; namespace jinja { @@ -370,7 +370,7 @@ caps caps_get(jinja::program & prog) { caps_try_execute( prog, [&]() { - json args = json(R"({"arg": "value"})"); + json args = json::make(R"({"arg": "value"})"); if (result.supports_object_arguments) { args = json{{"arg", "value"}}; } diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 870596d617fb..cd92a804bc92 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -3,7 +3,7 @@ #include "value.h" // for converting from JSON to jinja values -#include +#include "json.h" #include #include @@ -1355,7 +1355,7 @@ const func_builtins & value_undefined_t::get_builtins() const { ////////////////////////////////// -static value from_json(const nlohmann::ordered_json & j, bool mark_input) { +static value from_json(const common_json_ref & j, bool mark_input) { if (j.is_null()) { return mk_val(); } else if (j.is_boolean()) { @@ -1378,8 +1378,8 @@ static value from_json(const nlohmann::ordered_json & j, bool mark_input) { return arr; } else if (j.is_object()) { auto obj = mk_val(); - for (auto it = j.begin(); it != j.end(); ++it) { - obj->insert(it.key(), from_json(it.value(), mark_input)); + for (const auto & [key, val] : j.items()) { + obj->insert(key, from_json(val, mark_input)); } return obj; } else { @@ -1451,18 +1451,20 @@ bool value_compare(const value & a, const value & b, value_compare_op op) { return result; } -template<> -void global_from_json(context & ctx, const nlohmann::ordered_json & json_obj, bool mark_input) { - // printf("global_from_json: %s\n" , json_obj.dump(2).c_str()); +template +void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input) { if (json_obj.is_null() || !json_obj.is_object()) { throw std::runtime_error("global_from_json: input JSON value must be an object"); } - for (auto it = json_obj.begin(); it != json_obj.end(); ++it) { - JJ_DEBUG("global_from_json: setting key '%s'", it.key().c_str()); - ctx.set_val(it.key(), from_json(it.value(), mark_input)); + for (const auto & [key, val] : json_obj.items()) { + JJ_DEBUG("global_from_json: setting key '%s'", key.c_str()); + ctx.set_val(key, from_json(val, mark_input)); } } +template void global_from_json (context &, const common_json &, bool); +template void global_from_json(context &, const common_json_ref &, bool); + // recursively convert value to JSON string // TODO: avoid circular references static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) { diff --git a/common/jinja/value.h b/common/jinja/value.h index 5cf85e4f5443..bf81cdc4a348 100644 --- a/common/jinja/value.h +++ b/common/jinja/value.h @@ -86,7 +86,7 @@ struct context; // forward declaration // marking input can be useful for tracking data provenance // and preventing template injection attacks // -// Note: T_JSON can be nlohmann::ordered_json +// Note: T_JSON can be common_json or common_json_ref template void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input); diff --git a/common/json.cpp b/common/json.cpp index aada499fe618..9f88b7170873 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -15,10 +15,6 @@ static ordered_json & as_json(common_json_node * node) { return *reinterpret_cast(node); } -static const ordered_json & as_json(const common_json_node * node) { - return *reinterpret_cast(node); -} - static common_json_node * as_node(ordered_json * json) { return reinterpret_cast(json); } @@ -59,10 +55,15 @@ template common_json common_json_from_raw(const T & json) { return make_json(json); } +template common_json_ref common_json_ref_from_raw(T & json) { + return common_json_ref(as_node(&json)); +} + // the bridge is usable only for the type below template ordered_json & common_json_raw(common_json_ref &); template const ordered_json & common_json_raw(const common_json_ref &); template common_json common_json_from_raw(const ordered_json &); +template common_json_ref common_json_ref_from_raw(ordered_json &); common_json_value::common_json_value(const char * val) { if (val) { @@ -76,6 +77,9 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} +common_json_value::common_json_value(std::initializer_list items) : + type(VAL_JSON), val_json(std::make_shared(items)) {} + common_json_value::common_json_value(const common_json_ref & val) : type(VAL_JSON), val_json(std::make_shared(make_json(as_json(val.get_node())))) {} @@ -86,6 +90,7 @@ bool common_json_ref::is_string() const { return as_json(node).is_string bool common_json_ref::is_boolean() const { return as_json(node).is_boolean(); } bool common_json_ref::is_number() const { return as_json(node).is_number(); } bool common_json_ref::is_number_integer() const { return as_json(node).is_number_integer(); } +bool common_json_ref::is_number_float() const { return as_json(node).is_number_float(); } bool common_json_ref::empty() const { return as_json(node).empty(); } size_t common_json_ref::size() const { return as_json(node).size(); } @@ -199,6 +204,16 @@ common_json common_json::array() { return make_json(ordered_json::array()); } +common_json common_json::array(std::initializer_list vals) { + ordered_json out = ordered_json::array(); + + for (const auto & val : vals) { + out.push_back(to_json(val)); + } + + return make_json(out); +} + common_json common_json::object() { return common_json(); } diff --git a/common/json.h b/common/json.h index ab4163790e24..3b37973dd0f6 100644 --- a/common/json.h +++ b/common/json.h @@ -1,6 +1,7 @@ #pragma once // JSON object, it works without the need to include a JSON library header +// the underlay library is pimpl, it should never be exposed here // note: object keys keep the order in which they are added #include @@ -15,6 +16,9 @@ class common_json; class common_json_ref; +// common_json_value holds a list of these, and each of them holds a value, so one must come first +struct common_json_item; + // one value of the backing library, only json.cpp knows what it is struct common_json_node; @@ -57,6 +61,9 @@ struct common_json_value { common_json_value(const common_json & val); common_json_value(const common_json_ref & val); + // nested object, e.g. {"fn", {{"name", "x"}}} + common_json_value(std::initializer_list items); + template ::value && !std::is_same::value, int>::type = 0> common_json_value(T val) : type(std::is_signed::value ? VAL_INT : VAL_UINT) { if (std::is_signed::value) { @@ -77,6 +84,10 @@ struct common_json_item { template common_json_item(std::string key, T && val) : key(std::move(key)), val(std::forward(val)) {} + + // a braced list cannot deduce T, so it needs its own overload + common_json_item(std::string key, std::initializer_list items) : + key(std::move(key)), val(items) {} }; // view to a value owned by a common_json, it goes stale if the owner gets a new key @@ -96,6 +107,7 @@ class common_json_ref { bool is_boolean() const; bool is_number() const; bool is_number_integer() const; + bool is_number_float() const; bool empty() const; size_t size() const; @@ -170,9 +182,9 @@ class common_json_ref { std::pair operator*() const; iterator & operator++() { - idx++; - return *this; - } + idx++; + return *this; + } bool operator!=(const iterator & other) const { return idx != other.idx; } @@ -215,6 +227,7 @@ class common_json : public common_json_ref { static common_json parse(const std::string & text); static common_json array(); + static common_json array(std::initializer_list vals); static common_json object(); // holds a single value, e.g. make("abc").dump() gives "\"abc\"" @@ -232,3 +245,6 @@ template T & common_json_raw(common_json_ref & json); template const T & common_json_raw(const common_json_ref & json); template common_json common_json_from_raw(const T & json); + +// view over a value of the backing library, it does not copy +template common_json_ref common_json_ref_from_raw(T & json); From d1a9e5a8e6ca5ab18e4c72df1476df2239e27608 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 21:05:03 +0200 Subject: [PATCH 04/19] migrate server --- common/chat.cpp | 2 +- common/chat.h | 1 - common/jinja/value.cpp | 5 +- common/jinja/value.h | 2 +- common/json.cpp | 262 +++++++++++++++++++-------------- common/json.h | 149 ++++++++++--------- tools/server/server-chat.h | 3 +- tools/server/server-common.cpp | 33 +++-- tools/server/server-common.h | 10 +- tools/server/server-context.h | 2 +- tools/server/server-task.h | 1 - 11 files changed, 263 insertions(+), 207 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 0461c00234b1..ff5f2a97f8fb 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -973,7 +973,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } - jinja::global_from_json(ctx, common_json_ref_from_raw(inp), inputs.mark_input); + jinja::global_from_json(ctx, common_json_from_raw(inp), inputs.mark_input); // render jinja::runtime runtime(ctx); diff --git a/common/chat.h b/common/chat.h index 6d5b220aebb5..7e1dc13dc901 100644 --- a/common/chat.h +++ b/common/chat.h @@ -17,7 +17,6 @@ #include using chat_template_caps = jinja::caps; -using json = nlohmann::ordered_json; struct common_chat_templates; diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index cd92a804bc92..1af09d6628cd 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -1355,7 +1355,7 @@ const func_builtins & value_undefined_t::get_builtins() const { ////////////////////////////////// -static value from_json(const common_json_ref & j, bool mark_input) { +static value from_json(const common_json & j, bool mark_input) { if (j.is_null()) { return mk_val(); } else if (j.is_boolean()) { @@ -1462,8 +1462,7 @@ void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input) { } } -template void global_from_json (context &, const common_json &, bool); -template void global_from_json(context &, const common_json_ref &, bool); +template void global_from_json(context &, const common_json &, bool); // recursively convert value to JSON string // TODO: avoid circular references diff --git a/common/jinja/value.h b/common/jinja/value.h index bf81cdc4a348..4926fb68016d 100644 --- a/common/jinja/value.h +++ b/common/jinja/value.h @@ -86,7 +86,7 @@ struct context; // forward declaration // marking input can be useful for tracking data provenance // and preventing template injection attacks // -// Note: T_JSON can be common_json or common_json_ref +// Note: T_JSON can be common_json template void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input); diff --git a/common/json.cpp b/common/json.cpp index 9f88b7170873..702d552f9503 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -6,27 +6,30 @@ #include #include +#include +#include #include using nlohmann::ordered_json; -// common_json_node is never defined, it only stands for an ordered_json in this file -static ordered_json & as_json(common_json_node * node) { - return *reinterpret_cast(node); +// a common_json is the backing value, so any value of a tree can be used as a common_json +static_assert(sizeof(ordered_json) <= sizeof(common_json), "common_json storage is too small"); +static_assert(alignof(ordered_json) <= alignof(common_json), "common_json alignment is too weak"); + +static ordered_json & as_json(common_json * self) { + return *reinterpret_cast(self); } -static common_json_node * as_node(ordered_json * json) { - return reinterpret_cast(json); +static const ordered_json & as_json(const common_json * self) { + return *reinterpret_cast(self); } -void common_json_node_deleter::operator()(common_json_node * node) const { - delete reinterpret_cast(node); +static common_json & as_common(ordered_json & json) { + return *reinterpret_cast(&json); } -static common_json make_json(const ordered_json & val) { - common_json out; - as_json(out.get_node()) = val; - return out; +static const common_json & as_common(const ordered_json & json) { + return *reinterpret_cast(&json); } static ordered_json to_json(const common_json_value & val) { @@ -37,33 +40,33 @@ static ordered_json to_json(const common_json_value & val) { case common_json_value::VAL_UINT: return val.val_uint; case common_json_value::VAL_DOUBLE: return val.val_double; case common_json_value::VAL_STRING: return val.val_string; - case common_json_value::VAL_JSON: return as_json(val.val_json->get_node()); + case common_json_value::VAL_JSON: return as_json(val.val_json.get()); } return nullptr; } -template T & common_json_raw(common_json_ref & json) { - return as_json(json.get_node()); +template T & common_json_raw(common_json & json) { + return as_json(&json); } -template const T & common_json_raw(const common_json_ref & json) { - return as_json(json.get_node()); +template const T & common_json_raw(const common_json & json) { + return as_json(&json); } template common_json common_json_from_raw(const T & json) { - return make_json(json); + return common_json(as_common(json)); } -template common_json_ref common_json_ref_from_raw(T & json) { - return common_json_ref(as_node(&json)); +template common_json & common_json_ref_from_raw(T & json) { + return as_common(json); } // the bridge is usable only for the type below -template ordered_json & common_json_raw(common_json_ref &); -template const ordered_json & common_json_raw(const common_json_ref &); +template ordered_json & common_json_raw(common_json &); +template const ordered_json & common_json_raw(const common_json &); template common_json common_json_from_raw(const ordered_json &); -template common_json_ref common_json_ref_from_raw(ordered_json &); +template common_json & common_json_ref_from_raw(ordered_json &); common_json_value::common_json_value(const char * val) { if (val) { @@ -77,158 +80,196 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} -common_json_value::common_json_value(std::initializer_list items) : - type(VAL_JSON), val_json(std::make_shared(items)) {} +common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { + common_json out = common_json::array(); -common_json_value::common_json_value(const common_json_ref & val) : - type(VAL_JSON), val_json(std::make_shared(make_json(as_json(val.get_node())))) {} + for (const auto & val : vals) { + out.push_back(val); + } -bool common_json_ref::is_null() const { return as_json(node).is_null(); } -bool common_json_ref::is_object() const { return as_json(node).is_object(); } -bool common_json_ref::is_array() const { return as_json(node).is_array(); } -bool common_json_ref::is_string() const { return as_json(node).is_string(); } -bool common_json_ref::is_boolean() const { return as_json(node).is_boolean(); } -bool common_json_ref::is_number() const { return as_json(node).is_number(); } -bool common_json_ref::is_number_integer() const { return as_json(node).is_number_integer(); } -bool common_json_ref::is_number_float() const { return as_json(node).is_number_float(); } + val_json = std::make_shared(std::move(out)); +} -bool common_json_ref::empty() const { return as_json(node).empty(); } -size_t common_json_ref::size() const { return as_json(node).size(); } +common_json_value::common_json_value(std::initializer_list items) : + type(VAL_JSON), val_json(std::make_shared(items)) {} -bool common_json_ref::contains(const std::string & key) const { - return as_json(node).contains(key); +common_json::common_json() { + new (storage) ordered_json(ordered_json::object()); } -bool common_json_ref::operator==(const common_json_value & val) const { - return as_json(node) == to_json(val); +common_json::common_json(const common_json & other) { + new (storage) ordered_json(as_json(&other)); } -bool common_json_ref::operator!=(const common_json_value & val) const { - return !(*this == val); +common_json::common_json(common_json && other) noexcept { + new (storage) ordered_json(std::move(as_json(&other))); } -common_json_ref common_json_ref::at(const std::string & key) const { - return common_json_ref(as_node(&as_json(node).at(key))); +common_json::common_json(std::initializer_list items) : common_json() { + for (const auto & item : items) { + set(item); + } } -common_json_ref common_json_ref::operator[](const std::string & key) const { - return common_json_ref(as_node(&as_json(node)[key])); +common_json::common_json(const common_json_value & val) { + new (storage) ordered_json(to_json(val)); } -common_json_ref common_json_ref::operator[](size_t idx) const { - return common_json_ref(as_node(&as_json(node)[idx])); +common_json::common_json(std::nullptr_t) { + new (storage) ordered_json(nullptr); } -void common_json_ref::assign(const common_json_value & val) { - as_json(node) = to_json(val); -} +common_json & common_json::operator=(const common_json & other) { + as_json(this) = as_json(&other); -void common_json_ref::set(const common_json_item & item) { - as_json(node)[item.key] = to_json(item.val); + return *this; } -void common_json_ref::push_back(const common_json_value & val) { - as_json(node).push_back(to_json(val)); +common_json & common_json::operator=(common_json && other) noexcept { + as_json(this) = std::move(as_json(&other)); + + return *this; } -std::string common_json_ref::dump(int indent) const { - return as_json(node).dump(indent); +common_json::~common_json() { + as_json(this).~basic_json(); } -// an array is indexed directly, an object needs a walk from the start -common_json_ref common_json_ref::iterator::operator*() const { - if (as_json(node).is_object()) { - return common_json_ref(as_node(&std::next(as_json(node).begin(), idx).value())); +common_json common_json::parse(const std::string & text) { + try { + return common_json_from_raw(ordered_json::parse(text)); + } catch (const std::exception & e) { + throw common_json_error(e.what()); } +} - return common_json_ref(as_node(&as_json(node)[idx])); +common_json common_json::array() { + return common_json_from_raw(ordered_json::array()); } -std::string common_json_ref::iterator::key() const { - return std::next(as_json(node).begin(), idx).key(); +common_json common_json::array(std::initializer_list vals) { + ordered_json out = ordered_json::array(); + + for (const auto & val : vals) { + out.push_back(to_json(val)); + } + + return common_json_from_raw(out); } -std::pair common_json_ref::items_view::iterator::operator*() const { - auto it = std::next(as_json(node).begin(), idx); +common_json common_json::object() { + return common_json(); +} - return { it.key(), common_json_ref(as_node(&it.value())) }; +common_json common_json::make(const common_json_value & val) { + return common_json(val); } -common_json::common_json() : - common_json_ref(nullptr), pimpl(as_node(new ordered_json(ordered_json::object()))) { - node = pimpl.get(); +bool common_json::is_null() const { return as_json(this).is_null(); } +bool common_json::is_object() const { return as_json(this).is_object(); } +bool common_json::is_array() const { return as_json(this).is_array(); } +bool common_json::is_string() const { return as_json(this).is_string(); } +bool common_json::is_boolean() const { return as_json(this).is_boolean(); } +bool common_json::is_number() const { return as_json(this).is_number(); } +bool common_json::is_number_integer() const { return as_json(this).is_number_integer(); } +bool common_json::is_number_float() const { return as_json(this).is_number_float(); } + +bool common_json::empty() const { return as_json(this).empty(); } +size_t common_json::size() const { return as_json(this).size(); } + +bool common_json::contains(const std::string & key) const { + return as_json(this).contains(key); } -common_json::common_json(std::initializer_list items) : common_json() { - for (const auto & item : items) { - set(item); - } +bool common_json::operator==(const common_json_value & val) const { + return as_json(this) == to_json(val); } -common_json::common_json(const common_json & other) : - common_json_ref(nullptr), pimpl(as_node(new ordered_json(as_json(other.node)))) { - node = pimpl.get(); +bool common_json::operator!=(const common_json_value & val) const { + return !(*this == val); } -common_json::common_json(common_json && other) noexcept : - common_json_ref(other.node), pimpl(std::move(other.pimpl)) { - other.node = nullptr; +common_json & common_json::at(const std::string & key) { return as_common(as_json(this).at(key)); } +const common_json & common_json::at(const std::string & key) const { return as_common(as_json(this).at(key)); } +common_json & common_json::at(size_t idx) { return as_common(as_json(this).at(idx)); } +const common_json & common_json::at(size_t idx) const { return as_common(as_json(this).at(idx)); } + +common_json & common_json::operator[](const std::string & key) { return as_common(as_json(this)[key]); } +const common_json & common_json::operator[](const std::string & key) const { return as_common(as_json(this).at(key)); } +common_json & common_json::operator[](size_t idx) { return as_common(as_json(this)[idx]); } +const common_json & common_json::operator[](size_t idx) const { return as_common(as_json(this).at(idx)); } + +common_json & common_json::front() { return as_common(as_json(this).front()); } +const common_json & common_json::front() const { return as_common(as_json(this).front()); } +common_json & common_json::back() { return as_common(as_json(this).back()); } +const common_json & common_json::back() const { return as_common(as_json(this).back()); } + +void common_json::erase(const std::string & key) { + as_json(this).erase(key); } -common_json & common_json::operator=(const common_json & other) { - as_json(node) = as_json(other.node); +void common_json::erase(size_t idx) { + as_json(this).erase(idx); +} - return *this; +void common_json::assign(const common_json_value & val) { + as_json(this) = to_json(val); } -common_json & common_json::operator=(common_json && other) noexcept { - pimpl = std::move(other.pimpl); - node = pimpl.get(); +void common_json::set(const common_json_item & item) { + as_json(this)[item.key] = to_json(item.val); +} - other.node = nullptr; +void common_json::push_back(const common_json_value & val) { + as_json(this).push_back(to_json(val)); +} - return *this; +std::string common_json::dump(int indent) const { + return as_json(this).dump(indent); } -common_json::~common_json() = default; +std::string common_json::dump_safe(int indent) const { + return as_json(this).dump(indent, ' ', false, ordered_json::error_handler_t::replace); +} -common_json common_json::parse(const std::string & text) { - try { - return make_json(ordered_json::parse(text)); - } catch (const std::exception & e) { - throw common_json_error(e.what()); +// an array is indexed directly, an object needs a walk from the start +common_json & common_json::iterator::operator*() const { + if (as_json(node).is_object()) { + return as_common(std::next(as_json(node).begin(), idx).value()); } -} -common_json common_json::array() { - return make_json(ordered_json::array()); + return as_common(as_json(node)[idx]); } -common_json common_json::array(std::initializer_list vals) { - ordered_json out = ordered_json::array(); +std::string common_json::iterator::key() const { + return std::next(as_json(node).begin(), idx).key(); +} - for (const auto & val : vals) { - out.push_back(to_json(val)); - } +common_json::iterator common_json::begin() const { + return iterator(const_cast(this), 0); +} - return make_json(out); +common_json::iterator common_json::end() const { + return iterator(const_cast(this), size()); } -common_json common_json::object() { - return common_json(); +common_json::items_view::entry common_json::items_view::iterator::operator*() const { + auto it = std::next(as_json(node).begin(), idx); + + return { it.key(), as_common(it.value()) }; } -common_json common_json::make(const common_json_value & val) { - return make_json(to_json(val)); +common_json::items_view common_json::items() const { + return items_view(const_cast(this), size()); } -template T common_json_ref::get() const { - return as_json(node).get(); +template T common_json::get() const { + return as_json(this).get(); } // get() is usable only for the types below -#define COMMON_JSON_GET(...) template __VA_ARGS__ common_json_ref::get<__VA_ARGS__>() const; +#define COMMON_JSON_GET(...) template __VA_ARGS__ common_json::get<__VA_ARGS__>() const; COMMON_JSON_GET(bool) COMMON_JSON_GET(int) @@ -241,5 +282,6 @@ COMMON_JSON_GET(float) COMMON_JSON_GET(double) COMMON_JSON_GET(std::string) COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::set) #undef COMMON_JSON_GET diff --git a/common/json.h b/common/json.h index 3b37973dd0f6..b435e4f3629f 100644 --- a/common/json.h +++ b/common/json.h @@ -2,6 +2,7 @@ // JSON object, it works without the need to include a JSON library header // the underlay library is pimpl, it should never be exposed here +// the backing value lives inside this object, so at() and the iterators give a real reference to it // note: object keys keep the order in which they are added #include @@ -12,20 +13,13 @@ #include #include #include +#include class common_json; -class common_json_ref; // common_json_value holds a list of these, and each of them holds a value, so one must come first struct common_json_item; -// one value of the backing library, only json.cpp knows what it is -struct common_json_node; - -struct common_json_node_deleter { - void operator()(common_json_node * node) const; -}; - struct common_json_error : std::runtime_error { using std::runtime_error::runtime_error; }; @@ -59,7 +53,7 @@ struct common_json_value { common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} common_json_value(const char * val); common_json_value(const common_json & val); - common_json_value(const common_json_ref & val); + common_json_value(const std::vector & vals); // nested object, e.g. {"fn", {{"name", "x"}}} common_json_value(std::initializer_list items); @@ -90,15 +84,31 @@ struct common_json_item { key(std::move(key)), val(items) {} }; -// view to a value owned by a common_json, it goes stale if the owner gets a new key -class common_json_ref { +class common_json { public: - explicit common_json_ref(common_json_node * node) : node(node) {} + common_json(); + common_json(const common_json & other); + common_json(common_json && other) noexcept; + common_json(std::initializer_list items); + common_json(const common_json_value & val); - common_json_ref(const common_json_ref &) = default; + // direct, a value would need two conversions in a row + common_json(std::nullptr_t); - // rebinding a view is almost always a write-through by mistake, use assign() to write - common_json_ref & operator=(const common_json_ref &) = delete; + common_json & operator=(const common_json & other); + common_json & operator=(common_json && other) noexcept; + + ~common_json(); + + // throws common_json_error if the text is not valid JSON + static common_json parse(const std::string & text); + + static common_json array(); + static common_json array(std::initializer_list vals); + static common_json object(); + + // holds a single value, e.g. make("abc").dump() gives "\"abc\"" + static common_json make(const common_json_value & val); bool is_null() const; bool is_object() const; @@ -118,9 +128,23 @@ class common_json_ref { bool operator!=(const common_json_value & val) const; // at() throws if the key is missing, operator[] adds a null value instead - common_json_ref at(const std::string & key) const; - common_json_ref operator[](const std::string & key) const; - common_json_ref operator[](size_t idx) const; + common_json & at(const std::string & key); + const common_json & at(const std::string & key) const; + common_json & at(size_t idx); + const common_json & at(size_t idx) const; + + common_json & operator[](const std::string & key); + const common_json & operator[](const std::string & key) const; + common_json & operator[](size_t idx); + const common_json & operator[](size_t idx) const; + + common_json & front(); + const common_json & front() const; + common_json & back(); + const common_json & back() const; + + void erase(const std::string & key); + void erase(size_t idx); // only for the types instantiated in json.cpp, the rest fails at link time template T get() const; @@ -138,21 +162,26 @@ class common_json_ref { void set(const common_json_item & item); void push_back(const common_json_value & val); - template - common_json_ref & operator=(T && val) { + // a common_json goes through the copy assignment above, everything else becomes a value + template ::type, common_json>::value, int>::type = 0> + common_json & operator=(T && val) { assign(common_json_value(std::forward(val))); return *this; } std::string dump(int indent = -1) const; + // same as dump(), but bad UTF-8 gets replaced instead of throwing + std::string dump_safe(int indent = -1) const; + // walks an array by index, or an object in insertion order class iterator { public: - iterator(common_json_node * node, size_t idx) : node(node), idx(idx) {} + iterator(common_json * node, size_t idx) : node(node), idx(idx) {} - common_json_ref operator*() const; - std::string key() const; + common_json & operator*() const; + common_json & value() const { return **this; } + std::string key() const; iterator & operator++() { idx++; @@ -163,23 +192,32 @@ class common_json_ref { bool operator==(const iterator & other) const { return idx == other.idx; } private: - common_json_node * node; - size_t idx; + common_json * node; + size_t idx; }; - iterator begin() const { return iterator(node, 0); } - iterator end() const { return iterator(node, size()); } + iterator begin() const; + iterator end() const; // allows: for (const auto & [key, val] : obj.items()) class items_view { public: - items_view(common_json_node * node, size_t n) : node(node), n(n) {} + // the members are public, so an entry also works with structured bindings + struct entry { + std::string k; + common_json & v; + + const std::string & key() const { return k; } + common_json & value() const { return v; } + }; + + items_view(common_json * node, size_t n) : node(node), n(n) {} class iterator { public: - iterator(common_json_node * node, size_t idx) : node(node), idx(idx) {} + iterator(common_json * node, size_t idx) : node(node), idx(idx) {} - std::pair operator*() const; + entry operator*() const; iterator & operator++() { idx++; @@ -189,62 +227,35 @@ class common_json_ref { bool operator!=(const iterator & other) const { return idx != other.idx; } private: - common_json_node * node; - size_t idx; + common_json * node; + size_t idx; }; iterator begin() const { return iterator(node, 0); } iterator end() const { return iterator(node, n); } private: - common_json_node * node; - size_t n; + common_json * node; + size_t n; }; - items_view items() const { return items_view(node, size()); } - - common_json_node * get_node() const { return node; } - - protected: - common_json_node * node; -}; - -// owns the value it points to -class common_json : public common_json_ref { - public: - common_json(); - common_json(std::initializer_list items); - common_json(const common_json & other); - common_json(common_json && other) noexcept; - - common_json & operator=(const common_json & other); - common_json & operator=(common_json && other) noexcept; - - // out-of-line, the deleter needs to know the real type - ~common_json(); - - // throws common_json_error if the text is not valid JSON - static common_json parse(const std::string & text); - - static common_json array(); - static common_json array(std::initializer_list vals); - static common_json object(); - - // holds a single value, e.g. make("abc").dump() gives "\"abc\"" - static common_json make(const common_json_value & val); + items_view items() const; private: - std::unique_ptr pimpl; + // the backing value is built here, json.cpp checks that it fits + alignas(8) unsigned char storage[32]; }; +using common_json_entry = common_json::items_view::entry; + // bridge for code that still uses internal component from nlohmann::json // usage: common_json_raw(j) // TODO: maybe completely remove this in the future -template T & common_json_raw(common_json_ref & json); -template const T & common_json_raw(const common_json_ref & json); +template T & common_json_raw(common_json & json); +template const T & common_json_raw(const common_json & json); template common_json common_json_from_raw(const T & json); // view over a value of the backing library, it does not copy -template common_json_ref common_json_ref_from_raw(T & json); +template common_json & common_json_ref_from_raw(T & json); diff --git a/tools/server/server-chat.h b/tools/server/server-chat.h index 102eae688a31..2b4945980c95 100644 --- a/tools/server/server-chat.h +++ b/tools/server/server-chat.h @@ -6,9 +6,8 @@ #include "server-common.h" #include "server-http.h" -#include +#include "json.h" -using json = nlohmann::ordered_json; // Convert OpenAI Responses API format to OpenAI Chat Completions API format json server_chat_convert_responses_to_chatcmpl(const json & body); diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index 585f65e83c65..196e342f01ad 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -9,6 +9,9 @@ #include "server-common.h" +// the chat API is not migrated yet, so this file still needs the bridge +#include + #include #include #include @@ -977,9 +980,9 @@ static server_tokens tokenize_input_subprompt(const llama_vocab * vocab, mtmd_co // JSON object with prompt and multimodal key. std::vector files; for (const auto & entry : json_prompt.at(JSON_MTMD_DATA_KEY)) { - files.push_back(base64_decode(entry)); + files.push_back(base64_decode(entry.get())); } - return process_mtmd_prompt(mctx, json_prompt.at(JSON_STRING_PROMPT_KEY), files); + return process_mtmd_prompt(mctx, json_prompt.at(JSON_STRING_PROMPT_KEY).get(), files); } else { // Not multimodal, but contains a subobject. llama_tokens tmp = tokenize_mixed(vocab, json_prompt.at(JSON_STRING_PROMPT_KEY), add_special, parse_special); @@ -1258,8 +1261,8 @@ json oaicompat_chat_params_parse( auto caps = common_chat_templates_get_caps(opt.tmpls.get()); common_chat_templates_inputs inputs; - inputs.messages = common_chat_msgs_parse_oaicompat(messages); - inputs.tools = common_chat_tools_parse_oaicompat(tools); + inputs.messages = common_chat_msgs_parse_oaicompat(common_json_raw(messages)); + inputs.tools = common_chat_tools_parse_oaicompat(common_json_raw(tools)); inputs.tool_choice = common_chat_tool_choice_parse_oaicompat(tool_choice); inputs.json_schema = json_schema.is_null() ? "" : json_schema.dump(); inputs.grammar = grammar; @@ -1267,7 +1270,7 @@ json oaicompat_chat_params_parse( inputs.parallel_tool_calls = json_value(body, "parallel_tool_calls", caps["supports_parallel_tool_calls"]); inputs.add_generation_prompt = json_value(body, "add_generation_prompt", true); inputs.continue_final_message = body.contains("continue_final_message") ? - common_chat_continuation_parse(body.at("continue_final_message")) : + common_chat_continuation_parse(common_json_raw(body.at("continue_final_message"))) : COMMON_CHAT_CONTINUATION_NONE; if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_NONE && opt.prefill_assistant && !inputs.messages.empty() && inputs.messages.back().role == "assistant") { @@ -1300,7 +1303,8 @@ json oaicompat_chat_params_parse( } // parse the "enable_thinking" kwarg to override the default value - auto enable_thinking_kwarg = json_value(inputs.chat_template_kwargs, "enable_thinking", std::string("")); + const auto kwarg_it = inputs.chat_template_kwargs.find("enable_thinking"); + std::string enable_thinking_kwarg = kwarg_it == inputs.chat_template_kwargs.end() ? "" : kwarg_it->second; if (enable_thinking_kwarg == "true") { inputs.enable_thinking = true; } else if (enable_thinking_kwarg == "false") { @@ -1316,7 +1320,7 @@ json oaicompat_chat_params_parse( inputs.enable_thinking = false; inputs.chat_template_kwargs.erase("reasoning_effort"); } else if (!reasoning_effort.empty()) { - inputs.chat_template_kwargs["reasoning_effort"] = json(reasoning_effort).dump(); + inputs.chat_template_kwargs["reasoning_effort"] = json::make(reasoning_effort).dump(); } } @@ -1347,7 +1351,7 @@ json oaicompat_chat_params_parse( llama_params["chat_parser"] = chat_params.parser; } - llama_params["message_delimiters"] = chat_params.message_delimiters.to_json(); + llama_params["message_delimiters"] = common_json_from_raw(chat_params.message_delimiters.to_json()); // Reasoning budget: pass parameters through to sampling layer { @@ -1465,7 +1469,10 @@ json format_response_rerank( }); elements.resize(std::min(top_n, (int)elements.size())); - json results = elements; + json results = json::array(); + for (const auto & el : elements) { + results.push_back(el); + } if (is_tei_format) return results; @@ -1540,7 +1547,7 @@ std::vector get_token_probabilities(llama_context * ctx, int i } std::string safe_json_to_str(const json & data) { - return data.dump(-1, ' ', false, json::error_handler_t::replace); + return data.dump_safe(); } // TODO: reuse llama_detokenize @@ -1790,12 +1797,12 @@ server_tokens format_prompt_rerank( std::string prompt = rerank_prompt; string_replace_all(prompt, "{query}" , query); string_replace_all(prompt, "{document}", doc ); - server_tokens tokens = tokenize_input_subprompt(vocab, mctx, prompt, false, true); + server_tokens tokens = tokenize_input_subprompt(vocab, mctx, json::make(prompt), false, true); result.push_back(tokens); } else { // Get EOS token - use SEP token as fallback if EOS is not available - server_tokens query_tokens = tokenize_input_subprompt(vocab, mctx, query, false, false); - server_tokens doc_tokens = tokenize_input_subprompt(vocab, mctx, doc, false, false); + server_tokens query_tokens = tokenize_input_subprompt(vocab, mctx, json::make(query), false, false); + server_tokens doc_tokens = tokenize_input_subprompt(vocab, mctx, json::make(doc), false, false); llama_token eos_token = llama_vocab_eos(vocab); if (eos_token == LLAMA_TOKEN_NULL) { eos_token = llama_vocab_sep(vocab); diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 6488be344c6a..48dbdfcb6306 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -7,7 +7,7 @@ #include "mtmd.h" #define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -19,7 +19,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; #define SLT_DBG(slot, fmt, ...) LOG_DBG("slot %12.*s: id %2d | task %d | " fmt, 12, __func__, (slot).id, ((slot).task ? (slot).task->id : -1), __VA_ARGS__) #define SLT_TRC(slot, fmt, ...) LOG_TRC("slot %12.*s: id %2d | task %d | " fmt, 12, __func__, (slot).id, ((slot).task ? (slot).task->id : -1), __VA_ARGS__) @@ -42,9 +42,9 @@ static T json_value(const json & body, const std::string & key, const T & defaul // Fallback null to default value if (body.contains(key) && !body.at(key).is_null()) { try { - return body.at(key); - } catch (NLOHMANN_JSON_NAMESPACE::detail::type_error const & err) { - LOG_WRN("Wrong type supplied for parameter '%s'. Expected '%s', using default value: %s\n", key.c_str(), json(default_value).type_name(), err.what()); + return body.at(key).get(); + } catch (const std::exception & err) { + LOG_WRN("Wrong type supplied for parameter '%s', using default value: %s\n", key.c_str(), err.what()); return default_value; } } else { diff --git a/tools/server/server-context.h b/tools/server/server-context.h index 764df0e08524..5d464b8e8cb7 100644 --- a/tools/server/server-context.h +++ b/tools/server/server-context.h @@ -4,7 +4,7 @@ #include "server-task.h" #include "server-queue.h" -#include +#include "json.h" #include #include diff --git a/tools/server/server-task.h b/tools/server/server-task.h index 25ff01512248..9c99143f8e19 100644 --- a/tools/server/server-task.h +++ b/tools/server/server-task.h @@ -11,7 +11,6 @@ // TODO: prevent including the whole server-common.h as we only use server_tokens #include "server-common.h" -using json = nlohmann::ordered_json; enum server_task_type { SERVER_TASK_TYPE_COMPLETION, From ea4b4b2862581601c3c2e49a646ca46302c49e70 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 22:22:14 +0200 Subject: [PATCH 05/19] big wip --- common/CMakeLists.txt | 1 + common/chat-auto-parser-generator.cpp | 9 ++- common/chat-auto-parser.h | 4 +- common/chat-diff-analyzer.cpp | 10 +-- common/chat-peg-parser.cpp | 13 ++-- common/chat-peg-parser.h | 12 ++-- common/chat.cpp | 85 +++++++++++++------------- common/chat.h | 18 +++--- common/json-schema-to-grammar.cpp | 26 ++++---- common/json-schema-to-grammar.h | 9 ++- common/json-shim.h | 16 +++++ common/json.cpp | 55 ++++++++++++++++- common/json.h | 44 +++++++++---- common/peg-parser.cpp | 29 +++++---- common/peg-parser.h | 10 +-- tools/cli/cli-context.cpp | 10 +-- tools/parser/debug-template-parser.cpp | 4 +- tools/parser/template-analysis.cpp | 4 +- tools/server/server-chat.cpp | 2 +- tools/server/server-common.cpp | 10 ++- tools/server/server-context.cpp | 32 +++++----- tools/server/server-models.cpp | 2 +- tools/server/server-schema.cpp | 9 ++- tools/server/server-task.cpp | 28 ++++----- tools/server/server-tools.cpp | 2 +- 25 files changed, 265 insertions(+), 179 deletions(-) create mode 100644 common/json-shim.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 36f1e0cd50f1..3f7d4b760d37 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -81,6 +81,7 @@ add_library(${TARGET} imatrix-loader.cpp imatrix-loader.h json-schema-to-grammar.cpp + json-shim.h json.cpp json.h llguidance.cpp diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index af84ff323daf..81a4ef0aee39 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -5,13 +5,12 @@ #include "common.h" #include "json-schema-to-grammar.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" #include #include -using json = nlohmann::ordered_json; +using json = common_json; // Helper to iterate over tools/functions static void foreach_function(const json & tools, const std::function & fn) { @@ -312,7 +311,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context foreach_function(inputs.tools, [&](const json & tool) { const auto & func = tool.at("function"); - std::string name = func.at("name"); + std::string name = func.at("name").get(); const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object(); // Build call_id parser based on position (if supported) @@ -385,13 +384,13 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte foreach_function(inputs.tools, [&](const json & tool) { const auto & func = tool.at("function"); - std::string name = func.at("name"); + std::string name = func.at("name").get(); auto params = func.contains("parameters") ? func.at("parameters") : json::object(); const auto & properties = params.contains("properties") ? params.at("properties") : json::object(); std::set required; if (params.contains("required")) { - params.at("required").get_to(required); + required = params.at("required").get>(); } auto schema_info = common_schema_info(); diff --git a/common/chat-auto-parser.h b/common/chat-auto-parser.h index 074216b11ee1..8ae15c91e1e5 100644 --- a/common/chat-auto-parser.h +++ b/common/chat-auto-parser.h @@ -4,7 +4,7 @@ #include "common.h" #include "jinja/caps.h" #include "peg-parser.h" -#include "nlohmann/json.hpp" +#include "json.h" #include #include @@ -12,7 +12,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; class common_chat_peg_builder; diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp index d6d2af2d50fe..79ce8153bf67 100644 --- a/common/chat-diff-analyzer.cpp +++ b/common/chat-diff-analyzer.cpp @@ -4,9 +4,9 @@ #include "chat.h" #include "common.h" #include "log.h" -#include "nlohmann/json.hpp" #include "peg-parser.h" +#include #include #include #include @@ -17,7 +17,7 @@ #define ANSI_ORANGE "\033[1m\x1b[38;5;214m" #define ANSI_RED "\033[1m\x1b[38;5;196m" -using json = nlohmann::ordered_json; +using json = common_json; namespace autoparser { @@ -929,10 +929,10 @@ void analyze_tools::analyze_tool_call_format_json_native(const std::string & cle int json_end = clean_haystack.find_last_of('}'); std::string cut = clean_haystack.substr(json_start, json_end - json_start + 1); json call_struct = json::parse(cut); - auto register_field = [&](const std::string & prefix, const nlohmann::detail::iteration_proxy_value & subel) { - if (subel.value().is_string() && std::string(subel.value()).find("call0000") != std::string::npos) { + auto register_field = [&](const std::string & prefix, const common_json_entry & subel) { + if (subel.value().is_string() && subel.value().get().find("call0000") != std::string::npos) { format.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key(); - } else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) { + } else if (subel.value().is_string() && subel.value().get() == fun_name_needle) { format.name_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key(); } else if (subel.value().dump().find(arg_name_needle) != std::string::npos) { // handle both string and JSON obj variants diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index 06737b165c05..6f41daa40389 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -4,12 +4,11 @@ #include "ggml.h" #include "peg-parser.h" -#include #include #include -using ordered_json = nlohmann::ordered_json; +using ordered_json = common_json; static std::string_view trim_trailing_space(std::string_view sv, int max = -1) { int count = 0; @@ -489,7 +488,7 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); // Build argument parsers @@ -566,7 +565,7 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto args = eps(); @@ -641,7 +640,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); // Build inner object fields @@ -727,7 +726,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() + @@ -796,7 +795,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto tool_name_ = name_key_parser + space() + literal(":") + space() + diff --git a/common/chat-peg-parser.h b/common/chat-peg-parser.h index 5d764dbaa0ec..114fa049fa74 100644 --- a/common/chat-peg-parser.h +++ b/common/chat-peg-parser.h @@ -128,7 +128,7 @@ class common_chat_peg_builder : public common_peg_parser_builder { // parameters_order: order in which JSON fields should be parsed common_peg_parser standard_json_tools(const std::string & section_start, const std::string & section_end, - const nlohmann::ordered_json & tools, + const common_json & tools, bool parallel_tool_calls, bool force_tool_calls, const std::string & name_key = "", @@ -143,13 +143,13 @@ class common_chat_peg_builder : public common_peg_parser_builder { // Legacy-compatible helper for building XML/tagged style tool calls // Used by tests and manual parsers common_peg_parser standard_constructed_tools(const std::map & markers, - const nlohmann::ordered_json & tools, + const common_json & tools, bool parallel_tool_calls, bool force_tool_calls); // Helper for Python-style function call format: name(arg1="value1", arg2=123) // Used by LFM2 and similar templates - common_peg_parser python_style_tool_calls(const nlohmann::ordered_json & tools, + common_peg_parser python_style_tool_calls(const common_json & tools, bool parallel_tool_calls, bool allow_json_literals); @@ -158,19 +158,19 @@ class common_chat_peg_builder : public common_peg_parser_builder { common_peg_parser python_or_json_value(); // Implementation helpers for standard_json_tools — one per JSON tool call layout mode - common_peg_parser build_json_tools_function_is_key(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_function_is_key(const common_json & tools, const std::string & args_key, const std::string & effective_args_key, const std::string & call_id_key, const std::string & gen_call_id_key); - common_peg_parser build_json_tools_nested_keys(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_nested_keys(const common_json & tools, const std::string & effective_name_key, const std::string & effective_args_key, const std::string & call_id_key, const std::string & gen_call_id_key); - common_peg_parser build_json_tools_flat_keys(const nlohmann::ordered_json & tools, + common_peg_parser build_json_tools_flat_keys(const common_json & tools, const std::string & effective_name_key, const std::string & effective_args_key, const std::string & call_id_key, diff --git a/common/chat.cpp b/common/chat.cpp index ff5f2a97f8fb..4409a2b2e794 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -14,7 +14,6 @@ #include "jinja/caps.h" #include "peg-parser.h" -#include "nlohmann/json.hpp" #include #include @@ -31,7 +30,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::string format_time(const std::chrono::system_clock::time_point & now, const std::string & format) { auto time = std::chrono::system_clock::to_time_t(now); @@ -49,7 +48,7 @@ static json safe_args_parse(const std::string & to_parse) { } try { return json::parse(stripped); - } catch (json::exception & e) { + } catch (const common_json_error & e) { return stripped; } } @@ -387,14 +386,14 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa if (!message.contains("role")) { throw std::invalid_argument("Missing 'role' in message: " + message.dump()); } - msg.role = message.at("role"); + msg.role = message.at("role").get(); auto has_content = message.contains("content"); auto has_tool_calls = message.contains("tool_calls"); if (has_content) { const auto & content = message.at("content"); if (content.is_string()) { - msg.content = content; + msg.content = content.get(); } else if (content.is_array()) { for (const auto & part : content) { if (!part.contains("type")) { @@ -405,8 +404,8 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa throw std::invalid_argument("Unsupported content part type: " + type.dump()); } common_chat_msg_content_part msg_part; - msg_part.type = type; - msg_part.text = part.at("text"); + msg_part.type = type.get(); + msg_part.text = part.at("text").get(); msg.content_parts.push_back(msg_part); } } else if (!content.is_null()) { @@ -432,15 +431,15 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa if (!fc.contains("name")) { throw std::invalid_argument("Missing tool call name: " + tool_call.dump()); } - tc.name = fc.at("name"); + tc.name = fc.at("name").get(); const auto & args = fc.at("arguments"); if (args.is_string()) { - tc.arguments = args; + tc.arguments = args.get(); } else { tc.arguments = args.dump(); } if (tool_call.contains("id")) { - tc.id = tool_call.at("id"); + tc.id = tool_call.at("id").get(); } msg.tool_calls.push_back(tc); } @@ -451,13 +450,13 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa "https://github.com/ggml-org/llama.cpp/issues/12279)"); } if (message.contains("reasoning_content")) { - msg.reasoning_content = message.at("reasoning_content"); + msg.reasoning_content = message.at("reasoning_content").get(); } if (message.contains("name")) { - msg.tool_name = message.at("name"); + msg.tool_name = message.at("name").get(); } if (message.contains("tool_call_id")) { - msg.tool_call_id = message.at("tool_call_id"); + msg.tool_call_id = message.at("tool_call_id").get(); } msgs.push_back(msg); @@ -489,17 +488,17 @@ struct messages_inp_normalizer { json normalized = json::array(); for (const auto & msg : messages) { json copy = msg; - auto it = copy.find("content"); - if (it != copy.end()) { - if (only_typed && it->is_string()) { - *it = json::array({ + if (copy.contains("content")) { + json & it = copy.at("content"); + if (only_typed && it.is_string()) { + it = json::array({ json{ {"type", "text"}, - {"text", it->get()}, + {"text", it.get()}, } }); - } else if (only_string && it->is_array()) { - *it = concat_content_parts(*it); + } else if (only_string && it.is_array()) { + it = concat_content_parts(it); } } normalized.push_back(std::move(copy)); @@ -596,7 +595,7 @@ std::vector common_chat_tools_parse_oaicompat(const json & too const auto & function = tool.at("function"); result.push_back({ - /* .name = */ function.at("name"), + /* .name = */ function.at("name").get(), /* .description = */ function.value("description", ""), /* .parameters = */ function.value("parameters", json::object()).dump(), }); @@ -609,7 +608,7 @@ std::vector common_chat_tools_parse_oaicompat(const json & too return result; } -common_chat_continuation common_chat_continuation_parse(const nlohmann::ordered_json & value) { +common_chat_continuation common_chat_continuation_parse(const common_json & value) { if (value.is_boolean() && value.get()) { return COMMON_CHAT_CONTINUATION_AUTO; } @@ -921,7 +920,7 @@ static void foreach_parameter(const json & const auto & props = params.at("properties"); std::set required; if (params.contains("required") && params.at("required").is_array()) { - params.at("required").get_to(required); + required = params.at("required").get>(); } for (const auto & [name, prop] : props.items()) { bool is_required = (required.find(name) != required.end()); @@ -938,7 +937,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::context ctx(tmpl.source()); // messages_override is already built for this template, do not touch its content parts - nlohmann::ordered_json inp = nlohmann::ordered_json{ + common_json inp = common_json{ {"messages", messages_override.has_value() ? *messages_override : messages_inp_normalizer(tmpl.original_caps()).normalize(inputs.messages)}, @@ -973,7 +972,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } - jinja::global_from_json(ctx, common_json_from_raw(inp), inputs.mark_input); + jinja::global_from_json(ctx, inp, inputs.mark_input); // render jinja::runtime runtime(ctx); @@ -1059,7 +1058,7 @@ static common_chat_params common_chat_params_init_ministral_3(const common_chat_ }); } else if (msg.at("content").is_array()) { auto blocks = msg.at("content"); - content.insert(content.end(), blocks.begin(), blocks.end()); + content.insert(blocks); } } @@ -1114,7 +1113,7 @@ static common_chat_params common_chat_params_init_ministral_3(const common_chat_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & schema = function.at("parameters"); tool_choice |= @@ -1222,7 +1221,7 @@ static common_chat_params common_chat_params_init_qwen3_coder(const common_chat_ // starting . The model may hallucinate a tool name, but it is preferable over // constraining on foreach_function(inputs.tools, [&](const json & tool) { - const std::string name = tool.at("function").at("name"); + const std::string name = tool.at("function").at("name").get(); tool_call_starts.push_back(""); }); @@ -1250,7 +1249,7 @@ static common_chat_params common_chat_params_init_qwen3_coder(const common_chat_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); auto parameters = function.contains("parameters") ? function.at("parameters") : json::object(); auto schema_info = common_schema_info(); @@ -1441,7 +1440,7 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & params = function.at("parameters"); auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name)); @@ -1607,7 +1606,7 @@ static common_chat_params common_chat_params_init_gemma4(const common_chat_templ foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); // TODO @aldehir : need to extend json-schema-to-grammar to produce more than JSON rules // const auto & params = function.at("parameters"); @@ -1706,7 +1705,7 @@ static common_chat_params common_chat_params_init_functionary_v3_2(const common_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & schema = function.at("parameters"); // Tool format: >>>function_name\n{json_args} @@ -1843,7 +1842,7 @@ static common_chat_params common_chat_params_init_kimi_k2(const common_chat_temp auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & schema = function.at("parameters"); // Match: functions.: @@ -2037,7 +2036,7 @@ static common_chat_params common_chat_params_init_gigachat_v3( auto tool_choice = p.choice(); for (const auto & tool : inputs.tools) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & schema = function.at("parameters"); auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\""); @@ -2233,13 +2232,13 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha if (has_tool_calls) { foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); const auto & props = params.contains("properties") ? params.at("properties") : json::object(); std::set required; if (params.contains("required")) { - params.at("required").get_to(required); + required = params.at("required").get>(); } auto schema_info = common_schema_info(); @@ -2468,7 +2467,7 @@ static common_chat_params common_chat_params_init_kimi_k3(const common_chat_temp auto tool_choices = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const json schema = function.contains("parameters") ? function.at("parameters") : json::object(); // arguments come one tag per key, with the JSON type in a type="..." @@ -2789,7 +2788,7 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto schema_info = common_schema_info(); @@ -2861,7 +2860,7 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t std::set required; if (schema.contains("required")) { - schema.at("required").get_to(required); + required = schema.at("required").get>(); } std::vector required_elements; @@ -2973,10 +2972,10 @@ static void system_message_not_supported(json & messages) { auto & second_msg = messages[1]; second_msg["content"] = first_msg.at("content").get() + "\n" + second_msg.at("content").get(); - messages.erase(messages.begin()); + messages.erase(0); } else { LOG_WRN("Removing system prompt due to template not supporting system role\n"); - messages.erase(messages.begin()); + messages.erase(0); } } } @@ -3243,7 +3242,7 @@ static common_chat_params common_chat_params_init_minicpm5(const common_chat_tem auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - const std::string name = function.at("name"); + const std::string name = function.at("name").get(); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto args = p.eps(); @@ -3389,7 +3388,7 @@ static common_chat_params common_chat_params_init_muse_glimmer(const common_chat auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - const std::string name = function.at("name"); + const std::string name = function.at("name").get(); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto args = p.eps(); diff --git a/common/chat.h b/common/chat.h index 7e1dc13dc901..cb39e3458f44 100644 --- a/common/chat.h +++ b/common/chat.h @@ -8,7 +8,7 @@ #include "jinja/runtime.h" #include "jinja/caps.h" -#include "nlohmann/json_fwd.hpp" +#include "json.h" #include #include @@ -86,7 +86,7 @@ struct common_chat_msg { std::string tool_name; std::string tool_call_id; - nlohmann::ordered_json to_json_oaicompat(bool concat_typed_text = false) const; + common_json to_json_oaicompat(bool concat_typed_text = false) const; std::string render_content(const std::string & delimiter = "\n\n") const; @@ -210,7 +210,7 @@ struct common_chat_msg_delimiters { // split tokens into message spans. skips maps a start index to a length of a region to jump over without matching common_chat_msg_spans split(const llama_tokens & tokens, const std::map & skips = {}) const; - nlohmann::ordered_json to_json() const; + common_json to_json() const; }; struct common_chat_tool { @@ -349,16 +349,16 @@ common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::strin bool common_chat_templates_support_enable_thinking(const common_chat_templates * chat_templates); // Parses a JSON array of messages in OpenAI's chat completion API format. -std::vector common_chat_msgs_parse_oaicompat(const nlohmann::ordered_json & messages); +std::vector common_chat_msgs_parse_oaicompat(const common_json & messages); -std::vector common_chat_tools_parse_oaicompat(const nlohmann::ordered_json & tools); +std::vector common_chat_tools_parse_oaicompat(const common_json & tools); -common_chat_continuation common_chat_continuation_parse(const nlohmann::ordered_json & value); +common_chat_continuation common_chat_continuation_parse(const common_json & value); // DEPRECATED: only used in tests -nlohmann::ordered_json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text = false); +common_json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text = false); -nlohmann::ordered_json common_chat_tools_to_json_oaicompat(const std::vector & tools); +common_json common_chat_tools_to_json_oaicompat(const std::vector & tools); // get template caps, useful for reporting to server /props endpoint std::map common_chat_templates_get_caps(const common_chat_templates * chat_templates); @@ -385,4 +385,4 @@ struct common_chat_prompt_preset { common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates); -common_chat_msg_delimiters common_chat_msg_delimiters_parse(const nlohmann::ordered_json & delimiters); +common_chat_msg_delimiters common_chat_msg_delimiters_parse(const common_json & delimiters); diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index e7b12aed8a76..0912cdd5d851 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -1,4 +1,6 @@ #include "json-schema-to-grammar.h" +// the grammar builder walks the schema with the library API +#include "json-shim.h" #include "common.h" #include @@ -12,7 +14,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::string build_repetition(const std::string & item_rule, int min_items, int max_items, const std::string & separator_rule = "") { auto has_max = max_items != std::numeric_limits::max(); @@ -843,7 +845,7 @@ class common_schema_converter { } } else if (n.is_object()) { if (n.contains("$ref")) { - std::string ref = n["$ref"]; + std::string ref = n["$ref"].get(); if (_refs.find(ref) == _refs.end()) { json target; if (ref.find("https://") == 0) { @@ -914,7 +916,7 @@ class common_schema_converter { std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; if (schema.contains("$ref")) { - return _add_rule(rule_name, _resolve_ref(schema["$ref"])); + return _add_rule(rule_name, _resolve_ref(schema["$ref"].get())); } if (schema.contains("oneOf") || schema.contains("anyOf")) { std::vector alt_schemas = schema.contains("oneOf") ? schema["oneOf"].get>() : schema["anyOf"].get>(); @@ -968,7 +970,7 @@ class common_schema_converter { const std::string& hybrid_name = name; std::function add_component = [&](const json & comp_schema, bool is_required) { if (comp_schema.contains("$ref")) { - add_component(_refs[comp_schema["$ref"]], is_required); + add_component(_refs[comp_schema["$ref"].get()], is_required); } else if (comp_schema.contains("properties")) { for (const auto & prop : comp_schema["properties"].items()) { properties.emplace_back(prop.key(), prop.value()); @@ -1031,7 +1033,7 @@ class common_schema_converter { return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " space \"]\""); } if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) { - return _visit_pattern(schema["pattern"], rule_name); + return _visit_pattern(schema["pattern"].get(), rule_name); } if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) { return _add_primitive(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid")); @@ -1111,7 +1113,7 @@ common_schema_info::~common_schema_info() = default; common_schema_info::common_schema_info(common_schema_info &&) noexcept = default; common_schema_info & common_schema_info::operator=(common_schema_info &&) noexcept = default; -void common_schema_info::resolve_refs(nlohmann::ordered_json & schema) { +void common_schema_info::resolve_refs(common_json & schema) { impl_->resolve_refs(schema, ""); } @@ -1119,7 +1121,7 @@ void common_schema_info::resolve_refs(nlohmann::ordered_json & schema) { // Some models emit raw string values rather than JSON-encoded strings for string parameters. // If any branch of the schema (via oneOf, anyOf, $ref, etc.) permits a string, this returns // true, allowing callers to handle the value as a raw string for simplicity. -bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schema) { +bool common_schema_info::resolves_to_string(const common_json & schema) { std::unordered_set visited_refs; std::function check = [&](const json & s) -> bool { @@ -1129,7 +1131,7 @@ bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schem // Handle $ref if (s.contains("$ref")) { - const std::string & ref = s["$ref"]; + const std::string ref = s["$ref"].get(); if (visited_refs.find(ref) != visited_refs.end()) { // Circular reference, assume not a string to be safe return false; @@ -1212,7 +1214,7 @@ bool common_schema_info::resolves_to_string(const nlohmann::ordered_json & schem // Check format - many formats imply string if (s.contains("format")) { - const std::string & fmt = s["format"]; + const std::string fmt = s["format"].get(); if (fmt == "date" || fmt == "time" || fmt == "date-time" || fmt == "uri" || fmt == "email" || fmt == "hostname" || fmt == "ipv4" || fmt == "ipv6" || fmt == "uuid" || @@ -1236,7 +1238,7 @@ std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) (void)force_gbnf; #endif // LLAMA_USE_LLGUIDANCE return build_grammar([&](const common_grammar_builder & callbacks) { - auto copy = common_json_raw(schema); + auto copy = common_json_raw(schema); callbacks.resolve_refs(copy); callbacks.add_schema("", copy); }); @@ -1248,10 +1250,10 @@ std::string build_grammar(const std::function #include #include @@ -26,14 +25,14 @@ class common_schema_info { common_schema_info(common_schema_info &&) noexcept; common_schema_info & operator=(common_schema_info &&) noexcept; - void resolve_refs(nlohmann::ordered_json & schema); - bool resolves_to_string(const nlohmann::ordered_json & schema); + void resolve_refs(common_json & schema); + bool resolves_to_string(const common_json & schema); }; struct common_grammar_builder { std::function add_rule; - std::function add_schema; - std::function resolve_refs; + std::function add_schema; + std::function resolve_refs; }; struct common_grammar_options { diff --git a/common/json-shim.h b/common/json-shim.h new file mode 100644 index 000000000000..9c8cda0fe789 --- /dev/null +++ b/common/json-shim.h @@ -0,0 +1,16 @@ +#pragma once + +// converts between common_json and the backing JSON library +// +// include this only in a cpp file that touches an internal component of the library, +// never in a header. every use here is a place to fix if the library changes. + +#include "json.h" + +template T & common_json_raw(common_json & json); +template const T & common_json_raw(const common_json & json); + +template common_json common_json_from_raw(const T & json); + +// view over a value of the backing library, it does not copy +template common_json & common_json_ref_from_raw(T & json); diff --git a/common/json.cpp b/common/json.cpp index 702d552f9503..dd0db5face41 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -1,4 +1,6 @@ #include "json.h" +// defines the shim +#include "json-shim.h" #include "ggml.h" @@ -80,7 +82,8 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} -common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { +template +common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { common_json out = common_json::array(); for (const auto & val : vals) { @@ -90,6 +93,23 @@ common_json_value::common_json_value(const std::vector & vals) : ty val_json = std::make_shared(std::move(out)); } +// a vector value is usable only for the types below +// note: std::vector is not here, its proxy reference does not convert +#define COMMON_JSON_VEC(...) template common_json_value::common_json_value(const std::vector<__VA_ARGS__> &); + +COMMON_JSON_VEC(int) +COMMON_JSON_VEC(unsigned int) +COMMON_JSON_VEC(long) +COMMON_JSON_VEC(unsigned long) +COMMON_JSON_VEC(long long) +COMMON_JSON_VEC(unsigned long long) +COMMON_JSON_VEC(float) +COMMON_JSON_VEC(double) +COMMON_JSON_VEC(std::string) +COMMON_JSON_VEC(common_json) + +#undef COMMON_JSON_VEC + common_json_value::common_json_value(std::initializer_list items) : type(VAL_JSON), val_json(std::make_shared(items)) {} @@ -143,6 +163,14 @@ common_json common_json::parse(const std::string & text) { } } +common_json common_json::parse_no_throw(const std::string & text) { + return common_json_from_raw(ordered_json::parse(text, nullptr, false)); +} + +bool common_json::is_discarded() const { + return as_json(this).is_discarded(); +} + common_json common_json::array() { return common_json_from_raw(ordered_json::array()); } @@ -161,6 +189,10 @@ common_json common_json::object() { return common_json(); } +common_json common_json::object(std::initializer_list items) { + return common_json(items); +} + common_json common_json::make(const common_json_value & val) { return common_json(val); } @@ -204,6 +236,10 @@ const common_json & common_json::front() const { return as_common(as_json(this). common_json & common_json::back() { return as_common(as_json(this).back()); } const common_json & common_json::back() const { return as_common(as_json(this).back()); } +void common_json::clear() { + as_json(this).clear(); +} + void common_json::erase(const std::string & key) { as_json(this).erase(key); } @@ -224,6 +260,22 @@ void common_json::push_back(const common_json_value & val) { as_json(this).push_back(to_json(val)); } +void common_json::push_back(std::initializer_list items) { + common_json val(items); + + as_json(this).push_back(as_json(&val)); +} + +size_t common_json::count(const std::string & key) const { + return as_json(this).count(key); +} + +void common_json::insert(const common_json & vals) { + ordered_json & self = as_json(this); + + self.insert(self.end(), as_json(&vals).begin(), as_json(&vals).end()); +} + std::string common_json::dump(int indent) const { return as_json(this).dump(indent); } @@ -283,5 +335,6 @@ COMMON_JSON_GET(double) COMMON_JSON_GET(std::string) COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::set) +COMMON_JSON_GET(std::vector) #undef COMMON_JSON_GET diff --git a/common/json.h b/common/json.h index b435e4f3629f..1b2f8adbbc32 100644 --- a/common/json.h +++ b/common/json.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -53,7 +54,8 @@ struct common_json_value { common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} common_json_value(const char * val); common_json_value(const common_json & val); - common_json_value(const std::vector & vals); + // only for the types instantiated in json.cpp, the rest fails at link time + template common_json_value(const std::vector & vals); // nested object, e.g. {"fn", {{"name", "x"}}} common_json_value(std::initializer_list items); @@ -95,6 +97,11 @@ class common_json { // direct, a value would need two conversions in a row common_json(std::nullptr_t); + // one step, so that "abc" or a vector can go straight into a common_json + template ::type, common_json>::value && + !std::is_same::type, common_json_value>::value, int>::type = 0> + common_json(T && val) : common_json(common_json_value(std::forward(val))) {} + common_json & operator=(const common_json & other); common_json & operator=(common_json && other) noexcept; @@ -103,9 +110,15 @@ class common_json { // throws common_json_error if the text is not valid JSON static common_json parse(const std::string & text); + // gives a discarded value instead of throwing, check it with is_discarded() + static common_json parse_no_throw(const std::string & text); + + bool is_discarded() const; + static common_json array(); static common_json array(std::initializer_list vals); static common_json object(); + static common_json object(std::initializer_list items); // holds a single value, e.g. make("abc").dump() gives "\"abc\"" static common_json make(const common_json_value & val); @@ -143,6 +156,8 @@ class common_json { common_json & back(); const common_json & back() const; + void clear(); + void erase(const std::string & key); void erase(size_t idx); @@ -162,6 +177,15 @@ class common_json { void set(const common_json_item & item); void push_back(const common_json_value & val); + // appends one object, e.g. push_back({{"a", 1}}) + void push_back(std::initializer_list items); + + // 1 if the key is there, 0 if not + size_t count(const std::string & key) const; + + // appends every value of another array + void insert(const common_json & vals); + // a common_json goes through the copy assignment above, everything else becomes a value template ::type, common_json>::value, int>::type = 0> common_json & operator=(T && val) { @@ -177,6 +201,12 @@ class common_json { // walks an array by index, or an object in insertion order class iterator { public: + using iterator_category = std::forward_iterator_tag; + using value_type = common_json; + using difference_type = std::ptrdiff_t; + using pointer = common_json *; + using reference = common_json &; + iterator(common_json * node, size_t idx) : node(node), idx(idx) {} common_json & operator*() const; @@ -247,15 +277,3 @@ class common_json { }; using common_json_entry = common_json::items_view::entry; - -// bridge for code that still uses internal component from nlohmann::json -// usage: common_json_raw(j) -// TODO: maybe completely remove this in the future - -template T & common_json_raw(common_json & json); -template const T & common_json_raw(const common_json & json); - -template common_json common_json_from_raw(const T & json); - -// view over a value of the backing library, it does not copy -template common_json & common_json_ref_from_raw(T & json); diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 4a4be7cf789f..96cc267b9d9b 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -1,4 +1,6 @@ #include "peg-parser.h" +// the interface takes common_json, the parser internals stay on the library +#include "json-shim.h" #include "common.h" #include "json-schema-to-grammar.h" @@ -1120,8 +1122,8 @@ common_peg_parser common_peg_parser_builder::chars(const std::string & classes, return wrap(arena_.add_parser(common_peg_chars_parser{classes, ranges, negated, min, max})); } -common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, const nlohmann::ordered_json & schema, bool raw) { - return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::make_shared(schema), raw})); +common_peg_parser common_peg_parser_builder::schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw) { + return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::make_shared(schema), raw})); } common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) { @@ -1805,8 +1807,8 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo } } -static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & variant) { - using json = nlohmann::json; +static nlohmann::ordered_json serialize_parser_variant(const common_peg_parser_variant & variant) { + using json = nlohmann::ordered_json; return std::visit([](const auto & p) -> json { using T = std::decay_t; @@ -1860,7 +1862,7 @@ static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & {"type", "schema"}, {"child", p.child}, {"name", p.name}, - {"schema", p.schema ? *p.schema : nullptr}, + {"schema", p.schema ? common_json_raw(*p.schema) : nlohmann::ordered_json(nullptr)}, {"raw", p.raw} }; } else if constexpr (std::is_same_v) { @@ -1888,19 +1890,19 @@ static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & }, variant); } -nlohmann::json common_peg_arena::to_json() const { - auto parsers = nlohmann::json::array(); +common_json common_peg_arena::to_json() const { + auto parsers = nlohmann::ordered_json::array(); for (const auto & parser : parsers_) { parsers.push_back(serialize_parser_variant(parser)); } - return nlohmann::json{ + return common_json_from_raw(nlohmann::ordered_json{ {"parsers", parsers}, {"rules", rules_}, {"root", root_} - }; + }); } -static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json & j) { +static common_peg_parser_variant deserialize_parser_variant(const nlohmann::ordered_json & j) { if (!j.contains("type") || !j["type"].is_string()) { throw std::runtime_error("Parser variant JSON missing or invalid 'type' field"); } @@ -2007,7 +2009,7 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json parser.child = j["child"].get(); parser.name = j["name"]; if (!j["schema"].is_null()) { - parser.schema = std::make_shared(j["schema"]); + parser.schema = std::make_shared(common_json_from_raw(j["schema"])); } parser.raw = j["raw"].get(); return parser; @@ -2069,7 +2071,8 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json throw std::runtime_error("Unknown parser type: " + type); } -common_peg_arena common_peg_arena::from_json(const nlohmann::json & j) { +common_peg_arena common_peg_arena::from_json(const common_json & j_in) { + const nlohmann::ordered_json & j = common_json_raw(j_in); if (!j.contains("parsers") || !j["parsers"].is_array()) { throw std::runtime_error("JSON missing or invalid 'parsers' array"); } @@ -2109,7 +2112,7 @@ std::string common_peg_arena::save() const { } void common_peg_arena::load(const std::string & data) { - *this = from_json(nlohmann::json::parse(data)); + *this = from_json(common_json::parse(data)); } common_peg_arena build_peg_parser(const std::function & fn) { diff --git a/common/peg-parser.h b/common/peg-parser.h index c198499dd934..ab095cc7d671 100644 --- a/common/peg-parser.h +++ b/common/peg-parser.h @@ -1,6 +1,6 @@ #pragma once -#include +#include "json.h" #include #include @@ -245,7 +245,7 @@ struct common_peg_until_parser { struct common_peg_schema_parser { common_peg_parser_id child; std::string name; - std::shared_ptr schema; + std::shared_ptr schema; // Indicates if the GBNF should accept a raw string that matches the schema. bool raw; @@ -332,8 +332,8 @@ class common_peg_arena { std::string dump(common_peg_parser_id id) const; - nlohmann::json to_json() const; - static common_peg_arena from_json(const nlohmann::json & j); + common_json to_json() const; + static common_peg_arena from_json(const common_json & j); std::string save() const; void load(const std::string & data); @@ -490,7 +490,7 @@ class common_peg_parser_builder { // Wraps a parser with JSON schema metadata for grammar generation. // Used internally to convert JSON schemas to GBNF grammar rules. - common_peg_parser schema(const common_peg_parser & p, const std::string & name, const nlohmann::ordered_json & schema, bool raw = false); + common_peg_parser schema(const common_peg_parser & p, const std::string & name, const common_json & schema, bool raw = false); // Creates a named rule, stores it in the grammar, and returns a ref. // If trigger=true, marks this rule as an entry point for lazy grammar generation. diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index 3d801b73d4c1..f6a9c74ac620 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -7,7 +7,7 @@ #include "console.h" #define JSON_ASSERT GGML_ASSERT -#include +#include "json.h" #include #include @@ -16,7 +16,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; struct cli_context_impl { json messages = json::array(); @@ -73,7 +73,7 @@ static std::string format_error_message(const json & err) { // err is the raw response body of a failed request; it may or may not be JSON static std::string format_error_message(const std::string & err) { - json parsed = json::parse(err, nullptr, false); + json parsed = json::parse_no_throw(err); if (!parsed.is_discarded()) { return format_error_message(parsed); } @@ -157,7 +157,7 @@ bool cli_context::init() { if (!list_and_ask_models()) { return false; } - } catch (const json::parse_error & e) { + } catch (const common_json_error & e) { ui::show_error(e.what()); ui::show_message("This might be caused by an incorrect server-base endpoint URL"); return false; @@ -364,7 +364,7 @@ bool cli_context::generate_completion(generated_content & content_out, cli_timin ui::assistant_turn a; std::string err = client.post_sse("/v1/chat/completions", body.dump(), should_stop, [&](const std::string & payload) { - json chunk = json::parse(payload, nullptr, false); + json chunk = json::parse_no_throw(payload); if (chunk.is_discarded()) { return; } diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp index 8a916f79c78e..abe427022332 100644 --- a/tools/parser/debug-template-parser.cpp +++ b/tools/parser/debug-template-parser.cpp @@ -5,7 +5,7 @@ #include "gguf.h" #include "jinja/runtime.h" #include "log.h" -#include "nlohmann/json.hpp" +#include "json.h" #include "peg-parser.h" #include @@ -15,7 +15,7 @@ #include #include -using json = nlohmann::ordered_json; +using json = common_json; enum class output_mode { ANALYSIS, // Only output analysis results (default) diff --git a/tools/parser/template-analysis.cpp b/tools/parser/template-analysis.cpp index bf898a2290f1..11225bd8c000 100644 --- a/tools/parser/template-analysis.cpp +++ b/tools/parser/template-analysis.cpp @@ -11,9 +11,9 @@ #include #include -#include "nlohmann/json.hpp" +#include "json.h" -using json = nlohmann::ordered_json; +using json = common_json; // ANSI color codes - using 256-color palette for brighter colors (all bold) #define ANSI_RESET "\033[0m" diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index 0322e54ccea8..a6fe3c6ba619 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -153,7 +153,7 @@ json server_chat_convert_responses_to_chatcmpl(const json & response_body) { prev_msg["content"] = json::array(); } auto & prev_content = prev_msg["content"]; - prev_content.insert(prev_content.end(), chatcmpl_content.begin(), chatcmpl_content.end()); + prev_content.insert(chatcmpl_content); } else { item.erase("status"); item.erase("type"); diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index 196e342f01ad..8111a3ba1079 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -9,8 +9,6 @@ #include "server-common.h" -// the chat API is not migrated yet, so this file still needs the bridge -#include #include #include @@ -1261,8 +1259,8 @@ json oaicompat_chat_params_parse( auto caps = common_chat_templates_get_caps(opt.tmpls.get()); common_chat_templates_inputs inputs; - inputs.messages = common_chat_msgs_parse_oaicompat(common_json_raw(messages)); - inputs.tools = common_chat_tools_parse_oaicompat(common_json_raw(tools)); + inputs.messages = common_chat_msgs_parse_oaicompat(messages); + inputs.tools = common_chat_tools_parse_oaicompat(tools); inputs.tool_choice = common_chat_tool_choice_parse_oaicompat(tool_choice); inputs.json_schema = json_schema.is_null() ? "" : json_schema.dump(); inputs.grammar = grammar; @@ -1270,7 +1268,7 @@ json oaicompat_chat_params_parse( inputs.parallel_tool_calls = json_value(body, "parallel_tool_calls", caps["supports_parallel_tool_calls"]); inputs.add_generation_prompt = json_value(body, "add_generation_prompt", true); inputs.continue_final_message = body.contains("continue_final_message") ? - common_chat_continuation_parse(common_json_raw(body.at("continue_final_message"))) : + common_chat_continuation_parse(body.at("continue_final_message")) : COMMON_CHAT_CONTINUATION_NONE; if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_NONE && opt.prefill_assistant && !inputs.messages.empty() && inputs.messages.back().role == "assistant") { @@ -1351,7 +1349,7 @@ json oaicompat_chat_params_parse( llama_params["chat_parser"] = chat_params.parser; } - llama_params["message_delimiters"] = common_json_from_raw(chat_params.message_delimiters.to_json()); + llama_params["message_delimiters"] = chat_params.message_delimiters.to_json(); // Reasoning budget: pass parameters through to sampling layer { diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 1293c8640267..1df9e4a1afa0 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -35,7 +35,6 @@ #include #endif -using json = nlohmann::ordered_json; constexpr int HTTP_POLLING_SECONDS = 1; @@ -4220,7 +4219,8 @@ std::unique_ptr server_routes::handle_completions_impl( // tasks.reserve(inputs.size()); // TODO: this is inaccurate due to child tasks // message delimiters for checkpointing - auto delimiters = common_chat_msg_delimiters_parse(json_value(data, "message_delimiters", json::array())); + json delims = json_value(data, "message_delimiters", json::array()); + auto delimiters = common_chat_msg_delimiters_parse(delims); delimiters.tokenize(ctx_server.vocab); for (size_t i = 0; i < inputs.size(); i++) { @@ -4483,8 +4483,8 @@ static json get_res_model_info(const server_context_meta & meta) { static json get_res_models(const server_context_meta & meta) { // note: do NOT use ctx_server here, otherwise it's not possible to use this during sleep - return { - {"models", { + return json{ + {"models", json::array({ { {"name", meta.model_name}, {"model", meta.model_name}, @@ -4493,23 +4493,23 @@ static json get_res_models(const server_context_meta & meta) { {"digest", ""}, // dummy value, llama.cpp does not support managing model file's hash {"type", "model"}, {"description", ""}, - {"tags", {""}}, - {"capabilities", meta.has_mtmd ? json({"completion","multimodal"}) : json({"completion"})}, + {"tags", json::array({""})}, + {"capabilities", meta.has_mtmd ? json::array({"completion","multimodal"}) : json::array({"completion"})}, {"parameters", ""}, {"details", { {"parent_model", ""}, {"format", "gguf"}, {"family", ""}, - {"families", {""}}, + {"families", json::array({""})}, {"parameter_size", ""}, {"quantization_level", ""} }} } - }}, + })}, {"object", "list"}, - {"data", { + {"data", json::array({ get_res_model_info(meta), - }} + })} }; } @@ -5045,7 +5045,7 @@ void server_routes::init_routes() { std::string content; if (body.count("tokens") != 0) { - const llama_tokens tokens = body.at("tokens"); + const llama_tokens tokens = body.at("tokens").get(); content = tokens_to_str(ctx_server.vocab, tokens); } @@ -5103,7 +5103,7 @@ void server_routes::init_routes() { std::vector tasks; tasks.reserve(documents.size()); for (size_t i = 0; i < documents.size(); i++) { - auto tmp = format_prompt_rerank(ctx_server.model_tgt, ctx_server.vocab, ctx_server.mctx, query, documents[i]); + auto tmp = format_prompt_rerank(ctx_server.model_tgt, ctx_server.vocab, ctx_server.mctx, query.get(), documents[i]); server_task task = server_task(SERVER_TASK_TYPE_RERANK); task.id = rd.get_new_id(); task.tokens = std::move(tmp); @@ -5207,7 +5207,7 @@ void server_routes::init_routes() { std::unique_ptr server_routes::handle_slots_save(const server_http_req & req, int id_slot) { auto res = create_response(); const json request_data = json::parse(req.body); - std::string filename = request_data.at("filename"); + std::string filename = request_data.at("filename").get(); if (!fs_validate_filename(filename)) { res->error(format_error_response("Invalid filename", ERROR_TYPE_INVALID_REQUEST)); return res; @@ -5243,7 +5243,7 @@ std::unique_ptr server_routes::handle_slots_save(const ser std::unique_ptr server_routes::handle_slots_restore(const server_http_req & req, int id_slot) { auto res = create_response(); const json request_data = json::parse(req.body); - std::string filename = request_data.at("filename"); + std::string filename = request_data.at("filename").get(); if (!fs_validate_filename(filename)) { res->error(format_error_response("Invalid filename", ERROR_TYPE_INVALID_REQUEST)); return res; @@ -5332,7 +5332,7 @@ std::unique_ptr server_routes::handle_embeddings_impl(cons bool use_base64 = false; if (body.count("encoding_format") != 0) { - const std::string & format = body.at("encoding_format"); + const std::string format = body.at("encoding_format").get(); if (format == "base64") { use_base64 = true; } else if (format != "float") { @@ -5352,7 +5352,7 @@ std::unique_ptr server_routes::handle_embeddings_impl(cons int embd_normalize = params.embd_normalize; if (body.count("embd_normalize") != 0) { - embd_normalize = body.at("embd_normalize"); + embd_normalize = body.at("embd_normalize").get(); if (meta->pooling_type == LLAMA_POOLING_TYPE_NONE) { SRV_DBG("embd_normalize is not supported by pooling type %d, ignoring it\n", meta->pooling_type); } diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index d60545194188..db0fac99527b 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -2462,7 +2462,7 @@ server_http_proxy::server_http_proxy( bool has_files = !files.empty(); if (has_files) { - json form_fields = json::parse(body, nullptr, false); + json form_fields = json::parse_no_throw(body); if (!form_fields.is_discarded()) { auto boundary = generate_multipart_boundary(); effective_body = build_multipart_body(form_fields, files, boundary); diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 33c4ecd46ebf..7b368ef78810 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -258,7 +258,7 @@ std::vector> make_llama_cmpl_schema(const common_params & try { auto schema = json_value(data, "json_schema", json::object()); SRV_DBG("JSON schema: %s\n", schema.dump(2).c_str()); - std::string grammar_str = json_schema_to_grammar(common_json_from_raw(schema)); + std::string grammar_str = json_schema_to_grammar(schema); SRV_DBG("Converted grammar: %s\n", grammar_str.c_str()); params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, std::move(grammar_str)}; } catch (const std::exception & e) { @@ -487,7 +487,7 @@ std::vector> make_llama_cmpl_schema(const common_params & const auto & stop = data.at("stop"); if (stop.is_array()) { for (const auto & word : stop) { - if (!word.empty()) ctx.params.antiprompt.push_back(word); + if (!word.empty()) ctx.params.antiprompt.push_back(word.get()); } } else if (stop.is_string()) { ctx.params.antiprompt.push_back(stop.get()); @@ -503,7 +503,7 @@ std::vector> make_llama_cmpl_schema(const common_params & ->set_handler([&](field_eval_context & ctx, const json & data) { const auto & samplers = data.at("samplers"); if (samplers.is_array()) { - ctx.params.sampling.samplers = common_sampler_types_from_names(samplers); + ctx.params.sampling.samplers = common_sampler_types_from_names(samplers.get>()); } else if (samplers.is_string()) { ctx.params.sampling.samplers = common_sampler_types_from_chars(samplers.get()); } @@ -580,8 +580,7 @@ static void handle_with_catch(const char * name, std::function func) { // treat a null value as absent so clients can send null to request the server default static bool has_value(const json & data, const char * n) { - auto it = data.find(n); - return it != data.end() && !it->is_null(); + return data.contains(n) && !data.at(n).is_null(); } template diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp index 258cdcf8fb86..2bca3dd74408 100644 --- a/tools/server/server-task.cpp +++ b/tools/server/server-task.cpp @@ -1,5 +1,6 @@ #include "server-task.h" + #include "build-info.h" #include "server-chat.h" #include "chat.h" @@ -12,7 +13,6 @@ #include -using json = nlohmann::ordered_json; // // task_params @@ -304,7 +304,7 @@ json completion_token_output::probs_vector_to_json(const std::vector::lowest() : std::log(x); } @@ -407,7 +407,7 @@ json server_task_result_cmpl_final::to_json_oaicompat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } return res; @@ -455,7 +455,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_chat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } return res; @@ -516,7 +516,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_chat_stream() { } if (stats.is_set()) { - deltas.back().push_back({"timings", stats.to_json()}); + deltas.back()["timings"] = stats.to_json(); } // extra fields for debugging purposes @@ -709,7 +709,7 @@ json server_task_result_cmpl_final::to_json_oaicompat_resp_stream() { }); if (stats.is_set()) { - server_sent_events.back().at("data").push_back({"timings", stats.to_json()}); + server_sent_events.back().at("data")["timings"] = stats.to_json(); } return server_sent_events; @@ -1061,10 +1061,10 @@ json server_task_result_cmpl_partial::to_json_non_oaicompat() { }; // populate the timings object when needed (usually for the last response or with timings_per_token enabled) if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } if (is_progress) { - res.push_back({"prompt_progress", progress.to_json()}); + res["prompt_progress"] = progress.to_json(); } if (!prob_output.probs.empty()) { res["completion_probabilities"] = completion_token_output::probs_vector_to_json({prob_output}, post_sampling_probs); @@ -1101,10 +1101,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat() { res["__verbose"] = to_json_non_oaicompat(); } if (stats.is_set()) { - res.push_back({"timings", stats.to_json()}); + res["timings"] = stats.to_json(); } if (is_progress) { - res.push_back({"prompt_progress", progress.to_json()}); + res["prompt_progress"] = progress.to_json(); } return res; @@ -1155,10 +1155,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat_chat() { } if (stats.is_set()) { - last_json.push_back({"timings", stats.to_json()}); + last_json["timings"] = stats.to_json(); } if (is_progress) { - last_json.push_back({"prompt_progress", progress.to_json()}); + last_json["prompt_progress"] = progress.to_json(); } } @@ -1305,10 +1305,10 @@ json server_task_result_cmpl_partial::to_json_oaicompat_resp() { if (!events.empty()) { json & data = events.back().at("data"); if (stats.is_set()) { - data.push_back({"timings", stats.to_json()}); + data["timings"] = stats.to_json(); } if (is_progress) { - data.push_back({"prompt_progress", progress.to_json()}); + data["prompt_progress"] = progress.to_json(); } } diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index b5c5c078ae33..12e9dbb8cfa9 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -2156,7 +2156,7 @@ void server_tools::setup(const std::vector & enabled_tools, res->status = 200; res->data = safe_json_to_str(result); } - } catch (const json::exception & e) { + } catch (const common_json_error & e) { res->status = 400; res->data = safe_json_to_str(format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST)); } catch (const std::invalid_argument & e) { From 039426a696432f176c25a97003b9c5217e861b69 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 22:29:03 +0200 Subject: [PATCH 06/19] migrate tests --- common/json-schema-to-grammar.cpp | 8 ++++++-- common/json.h | 5 +++++ tests/test-chat-auto-parser.cpp | 2 +- tests/test-chat-peg-parser.cpp | 22 +++++++++++----------- tests/test-chat-template.cpp | 8 ++++---- tests/test-chat.cpp | 4 ++-- tests/test-grammar-integration.cpp | 4 ++-- tests/test-jinja.cpp | 4 ++-- tests/test-json-schema-to-grammar.cpp | 8 ++++---- tests/test-model-resolution.cpp | 6 +++--- 10 files changed, 40 insertions(+), 31 deletions(-) diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 0912cdd5d851..1d067684406b 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -919,7 +919,11 @@ class common_schema_converter { return _add_rule(rule_name, _resolve_ref(schema["$ref"].get())); } if (schema.contains("oneOf") || schema.contains("anyOf")) { - std::vector alt_schemas = schema.contains("oneOf") ? schema["oneOf"].get>() : schema["anyOf"].get>(); + const json & alts = schema.contains("oneOf") ? schema.at("oneOf") : schema.at("anyOf"); + std::vector alt_schemas; + for (const auto & alt : alts) { + alt_schemas.push_back(alt); + } return _add_rule(rule_name, _generate_union_rule(name, alt_schemas)); } if (schema_type.is_array()) { @@ -1238,7 +1242,7 @@ std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) (void)force_gbnf; #endif // LLAMA_USE_LLGUIDANCE return build_grammar([&](const common_grammar_builder & callbacks) { - auto copy = common_json_raw(schema); + auto copy = schema; callbacks.resolve_refs(copy); callbacks.add_schema("", copy); }); diff --git a/common/json.h b/common/json.h index 1b2f8adbbc32..4a9f5a82f7b7 100644 --- a/common/json.h +++ b/common/json.h @@ -173,6 +173,11 @@ class common_json { return contains(key) ? at(key).get() : std::string(def); } + // a JSON default needs no get(), it is already the right type + common_json value(const std::string & key, const common_json & def) const { + return contains(key) ? at(key) : def; + } + void assign(const common_json_value & val); void set(const common_json_item & item); void push_back(const common_json_value & val); diff --git a/tests/test-chat-auto-parser.cpp b/tests/test-chat-auto-parser.cpp index 2209dcac84c5..f98e28108655 100644 --- a/tests/test-chat-auto-parser.cpp +++ b/tests/test-chat-auto-parser.cpp @@ -2157,7 +2157,7 @@ static void test_tagged_args_with_embedded_quotes(testing & t) { for (const auto & tool_def : tools) { if (!tool_def.contains("function")) { continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); const auto & params = function.at("parameters"); if (!params.contains("properties") || !params.at("properties").is_object()) { continue; } diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp index 3ab7a67b6a82..432b67870c94 100644 --- a/tests/test-chat-peg-parser.cpp +++ b/tests/test-chat-peg-parser.cpp @@ -11,9 +11,9 @@ #include #include -#include "nlohmann/json.hpp" +#include "json.h" -using json = nlohmann::ordered_json; +using json = common_json; static json create_tools(); static void test_example_native(testing & t); @@ -400,13 +400,13 @@ static void test_example_qwen3_coder(testing & t) { std::vector tool_parsers; for (const auto & def : tools) { auto function = def.at("function"); - std::string name = function.at("name"); + std::string name = function.at("name").get(); auto parameters = function.at("parameters"); auto properties = parameters.at("properties"); std::set required_properties; if (function.contains("required")) { - function.at("required").get_to(required_properties); + required_properties = function.at("required").get>(); } std::vector arg_parsers; @@ -661,8 +661,8 @@ void test_command7_parser_compare(testing & t) { "5. Provide a detailed cost breakdown that includes accommodation, transportation, meals, and entry fees " "to attractions."; - std::vector> tool_calls = { - { "call_0", "plan_trip", nlohmann::json::parse(R"({ + std::vector> tool_calls = { + { "call_0", "plan_trip", common_json::parse(R"({ "destination": "Japan", "duration": 14, "budget": 4000, @@ -686,16 +686,16 @@ void test_command7_parser_compare(testing & t) { if (!tool_calls.empty()) { tokens.emplace_back("<|START_ACTION|>"); - auto json = nlohmann::json::array(); + auto json = common_json::array(); for (const auto & tc : tool_calls) { - auto tc_json = nlohmann::json::object(); + auto tc_json = common_json::object(); tc_json["tool_call_id"] = std::get<0>(tc); tc_json["tool_name"] = std::get<1>(tc); tc_json["parameters"] = std::get<2>(tc); json.push_back(tc_json); } - auto tokenized = simple_tokenize(json.dump(-1, ' ', true)); + auto tokenized = simple_tokenize(json.dump(-1)); tokens.insert(tokens.end(), tokenized.begin(), tokenized.end()); tokens.emplace_back("<|END_ACTION|>"); @@ -737,7 +737,7 @@ static void test_prefix_tool_names(testing & t) { { { "arg1", { { "type", "integer" } } }, } }, - { "required", { "arg1" } }, + { "required", json::array({ "arg1" }) }, } }, } } }; @@ -757,7 +757,7 @@ static void test_prefix_tool_names(testing & t) { { "arg1", { { "type", "integer" } } }, { "arg2", { { "type", "integer" } } }, } }, - { "required", { "arg1" } }, + { "required", json::array({ "arg1" }) }, } }, } } }; diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index 6a6292cd0151..bcc574afe981 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include "json.h" #undef NDEBUG #include @@ -20,7 +20,7 @@ #include "jinja/lexer.h" #include "jinja/caps.h" -using json = nlohmann::ordered_json; +using json = common_json; static int main_automated_tests(void); @@ -304,8 +304,8 @@ void run_single(const std::string& contents, json input, bool use_common, bool d if (input.contains("eos_token")) { eos_token = input["eos_token"].get(); } - nlohmann::ordered_json msgs_json = input["messages"]; - nlohmann::ordered_json tools_json = input["tools"]; + common_json msgs_json = input["messages"]; + common_json tools_json = input["tools"]; auto messages = common_chat_msgs_parse_oaicompat(msgs_json); auto tools = common_chat_tools_parse_oaicompat(tools_json); auto output = format_using_common(contents, bos_token, eos_token, messages, tools); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index c4670da85302..7918f0ffcf48 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -19,12 +19,12 @@ #include #include #include -#include +#include "json.h" #include #include #include -using json = nlohmann::ordered_json; +using json = common_json; static std::ostream & operator<<(std::ostream & os, const common_chat_msg_diff & diff) { os << "{ content_delta: " << diff.content_delta << "; "; diff --git a/tests/test-grammar-integration.cpp b/tests/test-grammar-integration.cpp index 4d5d13dd0d38..eb4b7c78f50f 100644 --- a/tests/test-grammar-integration.cpp +++ b/tests/test-grammar-integration.cpp @@ -7,13 +7,13 @@ #include "../src/unicode.h" #include "../src/llama-grammar.h" -#include +#include "json.h" #include #include #include -using json = nlohmann::ordered_json; +using json = common_json; static llama_grammar * build_grammar_with_root(const std::string & grammar_str, const char * grammar_root) { return llama_grammar_init_impl(nullptr, grammar_str.c_str(), grammar_root, false, nullptr, 0, nullptr, 0); diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 1eb2a062b75f..4b68d5538306 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include "json.h" #include "subproc.h" #include "jinja/runtime.h" @@ -14,7 +14,7 @@ #include "testing.h" -using json = nlohmann::ordered_json; +using json = common_json; static void test_template(testing & t, const std::string & name, const std::string & tmpl, const json & vars, const std::string & expect); diff --git a/tests/test-json-schema-to-grammar.cpp b/tests/test-json-schema-to-grammar.cpp index 74b57cf1b669..214dbe1993b8 100755 --- a/tests/test-json-schema-to-grammar.cpp +++ b/tests/test-json-schema-to-grammar.cpp @@ -6,7 +6,7 @@ #include "../src/llama-grammar.h" -#include +#include "json.h" #include #include @@ -1442,7 +1442,7 @@ static void test_resolves_to_string() { auto test = [](const std::string & name, const std::string & schema_str, bool expected) { fprintf(stderr, "- %s\n", name.c_str()); common_schema_info info; - auto schema = nlohmann::ordered_json::parse(schema_str); + auto schema = common_json::parse(schema_str); info.resolve_refs(schema); bool result = info.resolves_to_string(schema); if (result != expected) { @@ -1517,7 +1517,7 @@ int main() { test_all("C++", [](const TestCase & tc) { try { - tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true)); + tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); } catch (const std::invalid_argument & ex) { fprintf(stderr, "Error: %s\n", ex.what()); @@ -1531,7 +1531,7 @@ int main() { auto run = [](const TestCase & tc) { fprintf(stderr, "- %s\n", tc.name.c_str()); try { - tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true)); + tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true)); tc.verify_status(SUCCESS); } catch (const std::invalid_argument & ex) { fprintf(stderr, "Error: %s\n", ex.what()); diff --git a/tests/test-model-resolution.cpp b/tests/test-model-resolution.cpp index 2437eeec608c..80b02fb70a1c 100644 --- a/tests/test-model-resolution.cpp +++ b/tests/test-model-resolution.cpp @@ -9,7 +9,7 @@ #include "http.h" #include "log.h" -#include +#include "json.h" #include #include @@ -55,7 +55,7 @@ static const char * COMMIT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; static void serve_repos(httplib::Server & server) { server.Get(R"(/api/models/(.+)/refs)", [](const httplib::Request & req, httplib::Response & res) { if (g_repos.count(req.matches[1])) { - res.set_content(nlohmann::json{{"branches", {{{"name", "main"}, {"targetCommit", COMMIT}}}}}.dump(), + res.set_content(common_json{{"branches", {{{"name", "main"}, {"targetCommit", COMMIT}}}}}.dump(), "application/json"); } else { res.status = 404; @@ -66,7 +66,7 @@ static void serve_repos(httplib::Server & server) { res.status = 404; return; } - auto files = nlohmann::json::array(); + auto files = common_json::array(); size_t i = 0; for (const auto & p : g_repos[req.matches[1]]) { char oid[41]; From 03eb25f39343f1c9ecc6f85ca4f9796a6ab7b230 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 22:43:39 +0200 Subject: [PATCH 07/19] wip --- common/chat-auto-parser-generator.cpp | 4 ++-- common/chat-peg-parser.cpp | 10 +++++----- common/chat.cpp | 28 +++++++++++++-------------- common/download.cpp | 2 +- common/hf-cache.cpp | 4 ++-- common/json-schema-to-grammar.cpp | 8 ++++---- common/json.h | 13 +++++++++++++ tests/gguf-model-data.cpp | 2 +- tests/test-chat-auto-parser.cpp | 2 +- tests/test-chat-peg-parser.cpp | 8 ++++---- tests/test-chat.cpp | 2 +- tools/cli/cli-context.cpp | 6 +++--- tools/server/server-chat.cpp | 4 ++-- tools/server/server-context.cpp | 6 +++--- tools/server/server-http.cpp | 2 +- tools/server/server-schema.cpp | 6 +++--- tools/server/server-stream.cpp | 2 +- tools/server/server-tools.cpp | 18 ++++++++--------- 18 files changed, 70 insertions(+), 57 deletions(-) diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index 81a4ef0aee39..d7e117e4d98b 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -311,7 +311,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context foreach_function(inputs.tools, [&](const json & tool) { const auto & func = tool.at("function"); - std::string name = func.at("name").get(); + std::string name = func.at("name"); const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object(); // Build call_id parser based on position (if supported) @@ -384,7 +384,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte foreach_function(inputs.tools, [&](const json & tool) { const auto & func = tool.at("function"); - std::string name = func.at("name").get(); + std::string name = func.at("name"); auto params = func.contains("parameters") ? func.at("parameters") : json::object(); const auto & properties = params.contains("properties") ? params.at("properties") : json::object(); diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index 6f41daa40389..e17134b6c2d3 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -488,7 +488,7 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); // Build argument parsers @@ -565,7 +565,7 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto args = eps(); @@ -640,7 +640,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); // Build inner object fields @@ -726,7 +726,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() + @@ -795,7 +795,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys( continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object(); auto tool_name_ = name_key_parser + space() + literal(":") + space() + diff --git a/common/chat.cpp b/common/chat.cpp index 4409a2b2e794..50358729baee 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -968,7 +968,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_preserve_reasoning(ctx, enabled); } if (inp.contains("reasoning_effort") && inp["reasoning_effort"].is_string() && !inp["reasoning_effort"].empty()) { - std::string reasoning_effort = inp["reasoning_effort"].get(); + std::string reasoning_effort = inp["reasoning_effort"]; jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } @@ -1113,7 +1113,7 @@ static common_chat_params common_chat_params_init_ministral_3(const common_chat_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & schema = function.at("parameters"); tool_choice |= @@ -1221,7 +1221,7 @@ static common_chat_params common_chat_params_init_qwen3_coder(const common_chat_ // starting . The model may hallucinate a tool name, but it is preferable over // constraining on foreach_function(inputs.tools, [&](const json & tool) { - const std::string name = tool.at("function").at("name").get(); + const std::string name = tool.at("function").at("name"); tool_call_starts.push_back(""); }); @@ -1249,7 +1249,7 @@ static common_chat_params common_chat_params_init_qwen3_coder(const common_chat_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); auto parameters = function.contains("parameters") ? function.at("parameters") : json::object(); auto schema_info = common_schema_info(); @@ -1440,7 +1440,7 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & params = function.at("parameters"); auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name)); @@ -1606,7 +1606,7 @@ static common_chat_params common_chat_params_init_gemma4(const common_chat_templ foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); // TODO @aldehir : need to extend json-schema-to-grammar to produce more than JSON rules // const auto & params = function.at("parameters"); @@ -1705,7 +1705,7 @@ static common_chat_params common_chat_params_init_functionary_v3_2(const common_ auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & schema = function.at("parameters"); // Tool format: >>>function_name\n{json_args} @@ -1842,7 +1842,7 @@ static common_chat_params common_chat_params_init_kimi_k2(const common_chat_temp auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & schema = function.at("parameters"); // Match: functions.: @@ -2036,7 +2036,7 @@ static common_chat_params common_chat_params_init_gigachat_v3( auto tool_choice = p.choice(); for (const auto & tool : inputs.tools) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & schema = function.at("parameters"); auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\""); @@ -2232,7 +2232,7 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha if (has_tool_calls) { foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); const auto & props = params.contains("properties") ? params.at("properties") : json::object(); @@ -2467,7 +2467,7 @@ static common_chat_params common_chat_params_init_kimi_k3(const common_chat_temp auto tool_choices = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const json schema = function.contains("parameters") ? function.at("parameters") : json::object(); // arguments come one tag per key, with the JSON type in a type="..." @@ -2788,7 +2788,7 @@ static common_chat_params common_chat_params_init_minimax_m3(const common_chat_t auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto schema_info = common_schema_info(); @@ -3242,7 +3242,7 @@ static common_chat_params common_chat_params_init_minicpm5(const common_chat_tem auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - const std::string name = function.at("name").get(); + const std::string name = function.at("name"); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto args = p.eps(); @@ -3388,7 +3388,7 @@ static common_chat_params common_chat_params_init_muse_glimmer(const common_chat auto tool_choice = p.choice(); foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); - const std::string name = function.at("name").get(); + const std::string name = function.at("name"); auto params = function.contains("parameters") ? function.at("parameters") : json::object(); auto args = p.eps(); diff --git a/common/download.cpp b/common/download.cpp index 4b28a708c86e..f091ec5112df 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -921,7 +921,7 @@ std::string common_docker_resolve_model(const std::string & docker) { if (manifest.contains("layers")) { for (const auto & layer : manifest["layers"]) { if (layer.contains("mediaType")) { - std::string media_type = layer["mediaType"].get(); + std::string media_type = layer["mediaType"]; if (media_type == "application/vnd.docker.ai.gguf.v3" || media_type.find("gguf") != std::string::npos) { gguf_digest = layer["digest"].get(); diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index 50d6dd6105c4..ce727594eec9 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -244,8 +244,8 @@ static std::string get_repo_commit(const std::string & repo_id, !branch.contains("targetCommit") || !branch["targetCommit"].is_string()) { continue; } - std::string _name = branch["name"].get(); - std::string _commit = branch["targetCommit"].get(); + std::string _name = branch["name"]; + std::string _commit = branch["targetCommit"]; if (!is_valid_subpath(refs_path, _name)) { LOG_WRN("%s: skip invalid branch: %s\n", __func__, _name.c_str()); diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 1d067684406b..bb1119cd7a2d 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -845,7 +845,7 @@ class common_schema_converter { } } else if (n.is_object()) { if (n.contains("$ref")) { - std::string ref = n["$ref"].get(); + std::string ref = n["$ref"]; if (_refs.find(ref) == _refs.end()) { json target; if (ref.find("https://") == 0) { @@ -947,7 +947,7 @@ class common_schema_converter { } if ((schema_type.is_null() || schema_type == "object") && (schema.contains("properties") || - (schema.contains("additionalProperties") && schema["additionalProperties"] != true))) { + (schema.contains("additionalProperties") && !schema["additionalProperties"].get()))) { std::unordered_set required; if (schema.contains("required") && schema["required"].is_array()) { for (const auto & item : schema["required"]) { @@ -1135,7 +1135,7 @@ bool common_schema_info::resolves_to_string(const common_json & schema) { // Handle $ref if (s.contains("$ref")) { - const std::string ref = s["$ref"].get(); + const std::string ref = s["$ref"]; if (visited_refs.find(ref) != visited_refs.end()) { // Circular reference, assume not a string to be safe return false; @@ -1218,7 +1218,7 @@ bool common_schema_info::resolves_to_string(const common_json & schema) { // Check format - many formats imply string if (s.contains("format")) { - const std::string fmt = s["format"].get(); + const std::string fmt = s["format"]; if (fmt == "date" || fmt == "time" || fmt == "date-time" || fmt == "uri" || fmt == "email" || fmt == "hostname" || fmt == "ipv4" || fmt == "ipv6" || fmt == "uuid" || diff --git a/common/json.h b/common/json.h index 4a9f5a82f7b7..9415d57b3693 100644 --- a/common/json.h +++ b/common/json.h @@ -148,6 +148,10 @@ class common_json { common_json & operator[](const std::string & key); const common_json & operator[](const std::string & key) const; + common_json & operator[](const char * key) { return (*this)[std::string(key)]; } + const common_json & operator[](const char * key) const { return (*this)[std::string(key)]; } + common_json & operator[](int idx) { return (*this)[(size_t) idx]; } + const common_json & operator[](int idx) const { return (*this)[(size_t) idx]; } common_json & operator[](size_t idx); const common_json & operator[](size_t idx) const; @@ -164,6 +168,15 @@ class common_json { // only for the types instantiated in json.cpp, the rest fails at link time template T get() const; + // implicit get() for plain values, so they can be assigned to their C++ type directly + // note: kept to this short list on purpose, a wider one makes j["key"] ambiguous + operator std::string() const { return get(); } + operator bool() const { return get(); } + operator int() const { return get(); } + operator int64_t() const { return get(); } + operator float() const { return get(); } + operator double() const { return get(); } + template T value(const std::string & key, T def) const { return contains(key) ? at(key).get() : def; diff --git a/tests/gguf-model-data.cpp b/tests/gguf-model-data.cpp index fe8b4ca76e7f..18e2955dfa66 100644 --- a/tests/gguf-model-data.cpp +++ b/tests/gguf-model-data.cpp @@ -475,7 +475,7 @@ static std::string detect_gguf_filename(const std::string & repo, const std::str for (const auto & sibling : j["siblings"]) { if (!sibling.contains("rfilename")) { continue; } - std::string fname = sibling["rfilename"].get(); + std::string fname = sibling["rfilename"]; if (fname.size() < 5 || fname.substr(fname.size() - 5) != ".gguf") { continue; } diff --git a/tests/test-chat-auto-parser.cpp b/tests/test-chat-auto-parser.cpp index f98e28108655..2209dcac84c5 100644 --- a/tests/test-chat-auto-parser.cpp +++ b/tests/test-chat-auto-parser.cpp @@ -2157,7 +2157,7 @@ static void test_tagged_args_with_embedded_quotes(testing & t) { for (const auto & tool_def : tools) { if (!tool_def.contains("function")) { continue; } const auto & function = tool_def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); const auto & params = function.at("parameters"); if (!params.contains("properties") || !params.at("properties").is_object()) { continue; } diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp index 432b67870c94..cede556cf34a 100644 --- a/tests/test-chat-peg-parser.cpp +++ b/tests/test-chat-peg-parser.cpp @@ -114,9 +114,9 @@ static json create_tools() { { "default", 5 } } }, { "category", { { "type", "string" }, - { "enum", { "api", "troubleshooting", "billing", "general" } }, + { "enum", json::array({ "api", "troubleshooting", "billing", "general" }) }, { "description", "Filter search by specific category." } } } } }, - { "required", { "query", "category" } }, + { "required", json::array({ "query", "category" }) }, { "additionalProperties", false } } }, { "strict", true } } } }; @@ -400,13 +400,13 @@ static void test_example_qwen3_coder(testing & t) { std::vector tool_parsers; for (const auto & def : tools) { auto function = def.at("function"); - std::string name = function.at("name").get(); + std::string name = function.at("name"); auto parameters = function.at("parameters"); auto properties = parameters.at("properties"); std::set required_properties; if (function.contains("required")) { - required_properties = function.at("required").get>(); + required_properties = function.at("required").get>(); } std::vector arg_parsers; diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 7918f0ffcf48..d43c14ffa37a 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -7052,7 +7052,7 @@ static void test_reasoning_budget_message_per_request() { if (!llama_params.contains("reasoning_budget_message")) { throw std::runtime_error("reasoning_budget_message missing from llama_params (thinking_end_tag may be empty for this template)"); } - std::string got = llama_params["reasoning_budget_message"].get(); + std::string got = llama_params["reasoning_budget_message"]; if (got != per_request_message) { throw std::runtime_error("Expected reasoning_budget_message='" + per_request_message + "', got '" + got + "'"); } diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index f6a9c74ac620..a1dffc577655 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -218,7 +218,7 @@ bool cli_context::list_and_ask_models() { if (!m.contains("id") || !m.at("id").is_string()) { continue; } - std::string name = m.at("id").get(); + std::string name = m.at("id"); std::string display = name; if (m.contains("aliases") && m.at("aliases").is_array()) { std::vector aliases; @@ -387,14 +387,14 @@ bool cli_context::generate_completion(generated_content & content_out, cli_timin } const auto & delta = choice.at("delta"); if (delta.contains("reasoning_content") && delta.at("reasoning_content").is_string()) { - const std::string text = delta.at("reasoning_content").get(); + const std::string text = delta.at("reasoning_content"); if (!text.empty()) { content_out.reasoning += text; a.push(ui::ASSISTANT_DISPLAY_MODE_REASONING, text); } } if (delta.contains("content") && delta.at("content").is_string()) { - const std::string text = delta.at("content").get(); + const std::string text = delta.at("content"); if (!text.empty()) { content_out.content += text; a.push(ui::ASSISTANT_DISPLAY_MODE_CONTENT, text); diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index a6fe3c6ba619..2411ec708916 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -679,12 +679,12 @@ json convert_transcriptions_to_chatcmpl( chatcmpl_body["stream"] = stream == "true"; if (inp_body.contains("max_tokens")) { - std::string inp = inp_body["max_tokens"].get(); + std::string inp = inp_body["max_tokens"]; chatcmpl_body["max_tokens"] = std::stoul(inp); } if (inp_body.contains("temperature")) { - std::string inp = inp_body["temperature"].get(); + std::string inp = inp_body["temperature"]; chatcmpl_body["temperature"] = std::stof(inp); } diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 1df9e4a1afa0..b78cbf07f798 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -5207,7 +5207,7 @@ void server_routes::init_routes() { std::unique_ptr server_routes::handle_slots_save(const server_http_req & req, int id_slot) { auto res = create_response(); const json request_data = json::parse(req.body); - std::string filename = request_data.at("filename").get(); + std::string filename = request_data.at("filename"); if (!fs_validate_filename(filename)) { res->error(format_error_response("Invalid filename", ERROR_TYPE_INVALID_REQUEST)); return res; @@ -5243,7 +5243,7 @@ std::unique_ptr server_routes::handle_slots_save(const ser std::unique_ptr server_routes::handle_slots_restore(const server_http_req & req, int id_slot) { auto res = create_response(); const json request_data = json::parse(req.body); - std::string filename = request_data.at("filename").get(); + std::string filename = request_data.at("filename"); if (!fs_validate_filename(filename)) { res->error(format_error_response("Invalid filename", ERROR_TYPE_INVALID_REQUEST)); return res; @@ -5332,7 +5332,7 @@ std::unique_ptr server_routes::handle_embeddings_impl(cons bool use_base64 = false; if (body.count("encoding_format") != 0) { - const std::string format = body.at("encoding_format").get(); + const std::string format = body.at("encoding_format"); if (format == "base64") { use_base64 = true; } else if (format != "float") { diff --git a/tools/server/server-http.cpp b/tools/server/server-http.cpp index 2ec137aa0786..249e71b4167e 100644 --- a/tools/server/server-http.cpp +++ b/tools/server/server-http.cpp @@ -785,7 +785,7 @@ void server_http_context::register_gcp_compat() const { try { json payload = instance; - const std::string format = payload.at("@requestFormat").get(); + const std::string format = payload.at("@requestFormat"); payload.erase("@requestFormat"); if (payload.contains("stream")) { diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 7b368ef78810..914c2b7021ae 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -306,7 +306,7 @@ std::vector> make_llama_cmpl_schema(const common_params & add((new field_str("generation_prompt")) ->set_desc("Generation prompt appended to the chat template output") ->set_handler([&](field_eval_context & ctx, const json & data) { - std::string s = data.at("generation_prompt").get(); + std::string s = data.at("generation_prompt"); ctx.params.chat_parser_params.generation_prompt = s; ctx.params.sampling.generation_prompt = s; })); @@ -399,13 +399,13 @@ std::vector> make_llama_cmpl_schema(const common_params & ctx.params.sampling.reasoning_budget_end.clear(); if (data.contains("reasoning_budget_end_tags")) { for (const auto & t : data.at("reasoning_budget_end_tags")) { - std::string tag = t.get(); + std::string tag = t; if (!tag.empty()) { ctx.params.sampling.reasoning_budget_end.push_back(common_tokenize(ctx.vocab, tag, false, true)); } } } else if (data.contains("reasoning_budget_end_tag")) { - std::string tag = data.at("reasoning_budget_end_tag").get(); + std::string tag = data.at("reasoning_budget_end_tag"); if (!tag.empty()) { ctx.params.sampling.reasoning_budget_end.push_back(common_tokenize(ctx.vocab, tag, false, true)); } diff --git a/tools/server/server-stream.cpp b/tools/server/server-stream.cpp index f6b9b8a9f4cc..a591fa27f566 100644 --- a/tools/server/server-stream.cpp +++ b/tools/server/server-stream.cpp @@ -510,7 +510,7 @@ server_http_context::handler_t server_stream_make_lookup_handler() { if (body.contains("conversation_ids") && body["conversation_ids"].is_array()) { for (const auto & v : body["conversation_ids"]) { if (v.is_string()) { - std::string id = v.get(); + std::string id = v; if (!id.empty()) { requested.push_back(std::move(id)); } diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index 12e9dbb8cfa9..82553fe01dd8 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -896,7 +896,7 @@ struct server_tool_read_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path").get(); + std::string path = params.at("path"); int start_line = json_value(params, "start_line", 1); int end_line = json_value(params, "end_line", -1); // -1 = no limit bool append_loc = json_value(params, "append_loc", false); @@ -1015,7 +1015,7 @@ struct server_tool_file_glob_search : server_tool { json invoke(json params, server_tool::stream *) const override { auto io = make_tools_io(params); - const std::string path = params.at("path").get(); + const std::string path = params.at("path"); std::string base = io->resolve(path); std::string include = json_value(params, "include", std::string("**")); @@ -1128,8 +1128,8 @@ struct server_tool_grep_search : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path").get(); - std::string pat_str = params.at("pattern").get(); + std::string path = params.at("path"); + std::string pat_str = params.at("pattern"); std::string include = json_value(params, "include", std::string("**")); std::string exclude = json_value(params, "exclude", std::string("")); bool show_lineno = json_value(params, "return_line_numbers", false); @@ -1271,7 +1271,7 @@ struct server_tool_exec_shell_command : server_tool { } json invoke(json params, server_tool::stream * st) const override { - std::string command = params.at("command").get(); + std::string command = params.at("command"); int timeout = json_value(params, "timeout", 10); size_t max_output = (size_t) json_value(params, "max_output_size", (int) SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE); @@ -1348,8 +1348,8 @@ struct server_tool_write_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path").get(); - std::string content = params.at("content").get(); + std::string path = params.at("path"); + std::string content = params.at("content"); auto io = make_tools_io(params); if (!io->write_file(path, content)) { @@ -1405,7 +1405,7 @@ struct server_tool_edit_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path").get(); + std::string path = params.at("path"); const json & edits_json = params.at("edits"); if (!edits_json.is_array() || edits_json.empty()) { @@ -2078,7 +2078,7 @@ void server_tools::setup(const std::vector & enabled_tools, auto res = std::make_unique(); try { json body = json::parse(req.body); - std::string tool_name = body.at("tool").get(); + std::string tool_name = body.at("tool"); json params = body.value("params", json::object()); bool stream = body.value("stream", false); From 173d7e804edd89b222b189782730b01278da23a5 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 22:56:42 +0200 Subject: [PATCH 08/19] revert some excessive changes --- common/chat.cpp | 2 +- common/download.cpp | 2 +- common/hf-cache.cpp | 4 ++-- tests/gguf-model-data.cpp | 2 +- tests/test-chat.cpp | 2 +- tools/cli/cli-context.cpp | 6 +++--- tools/server/server-chat.cpp | 4 ++-- tools/server/server-context.cpp | 4 ++-- tools/server/server-http.cpp | 2 +- tools/server/server-schema.cpp | 6 +++--- tools/server/server-stream.cpp | 2 +- tools/server/server-tools.cpp | 18 +++++++++--------- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 50358729baee..84f7ac8587d3 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -968,7 +968,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_preserve_reasoning(ctx, enabled); } if (inp.contains("reasoning_effort") && inp["reasoning_effort"].is_string() && !inp["reasoning_effort"].empty()) { - std::string reasoning_effort = inp["reasoning_effort"]; + std::string reasoning_effort = inp["reasoning_effort"].get(); jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } diff --git a/common/download.cpp b/common/download.cpp index f091ec5112df..4b28a708c86e 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -921,7 +921,7 @@ std::string common_docker_resolve_model(const std::string & docker) { if (manifest.contains("layers")) { for (const auto & layer : manifest["layers"]) { if (layer.contains("mediaType")) { - std::string media_type = layer["mediaType"]; + std::string media_type = layer["mediaType"].get(); if (media_type == "application/vnd.docker.ai.gguf.v3" || media_type.find("gguf") != std::string::npos) { gguf_digest = layer["digest"].get(); diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index ce727594eec9..50d6dd6105c4 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -244,8 +244,8 @@ static std::string get_repo_commit(const std::string & repo_id, !branch.contains("targetCommit") || !branch["targetCommit"].is_string()) { continue; } - std::string _name = branch["name"]; - std::string _commit = branch["targetCommit"]; + std::string _name = branch["name"].get(); + std::string _commit = branch["targetCommit"].get(); if (!is_valid_subpath(refs_path, _name)) { LOG_WRN("%s: skip invalid branch: %s\n", __func__, _name.c_str()); diff --git a/tests/gguf-model-data.cpp b/tests/gguf-model-data.cpp index 18e2955dfa66..fe8b4ca76e7f 100644 --- a/tests/gguf-model-data.cpp +++ b/tests/gguf-model-data.cpp @@ -475,7 +475,7 @@ static std::string detect_gguf_filename(const std::string & repo, const std::str for (const auto & sibling : j["siblings"]) { if (!sibling.contains("rfilename")) { continue; } - std::string fname = sibling["rfilename"]; + std::string fname = sibling["rfilename"].get(); if (fname.size() < 5 || fname.substr(fname.size() - 5) != ".gguf") { continue; } diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index d43c14ffa37a..7918f0ffcf48 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -7052,7 +7052,7 @@ static void test_reasoning_budget_message_per_request() { if (!llama_params.contains("reasoning_budget_message")) { throw std::runtime_error("reasoning_budget_message missing from llama_params (thinking_end_tag may be empty for this template)"); } - std::string got = llama_params["reasoning_budget_message"]; + std::string got = llama_params["reasoning_budget_message"].get(); if (got != per_request_message) { throw std::runtime_error("Expected reasoning_budget_message='" + per_request_message + "', got '" + got + "'"); } diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index a1dffc577655..f6a9c74ac620 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -218,7 +218,7 @@ bool cli_context::list_and_ask_models() { if (!m.contains("id") || !m.at("id").is_string()) { continue; } - std::string name = m.at("id"); + std::string name = m.at("id").get(); std::string display = name; if (m.contains("aliases") && m.at("aliases").is_array()) { std::vector aliases; @@ -387,14 +387,14 @@ bool cli_context::generate_completion(generated_content & content_out, cli_timin } const auto & delta = choice.at("delta"); if (delta.contains("reasoning_content") && delta.at("reasoning_content").is_string()) { - const std::string text = delta.at("reasoning_content"); + const std::string text = delta.at("reasoning_content").get(); if (!text.empty()) { content_out.reasoning += text; a.push(ui::ASSISTANT_DISPLAY_MODE_REASONING, text); } } if (delta.contains("content") && delta.at("content").is_string()) { - const std::string text = delta.at("content"); + const std::string text = delta.at("content").get(); if (!text.empty()) { content_out.content += text; a.push(ui::ASSISTANT_DISPLAY_MODE_CONTENT, text); diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index 2411ec708916..a6fe3c6ba619 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -679,12 +679,12 @@ json convert_transcriptions_to_chatcmpl( chatcmpl_body["stream"] = stream == "true"; if (inp_body.contains("max_tokens")) { - std::string inp = inp_body["max_tokens"]; + std::string inp = inp_body["max_tokens"].get(); chatcmpl_body["max_tokens"] = std::stoul(inp); } if (inp_body.contains("temperature")) { - std::string inp = inp_body["temperature"]; + std::string inp = inp_body["temperature"].get(); chatcmpl_body["temperature"] = std::stof(inp); } diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index b78cbf07f798..744ffccd3a52 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -5103,7 +5103,7 @@ void server_routes::init_routes() { std::vector tasks; tasks.reserve(documents.size()); for (size_t i = 0; i < documents.size(); i++) { - auto tmp = format_prompt_rerank(ctx_server.model_tgt, ctx_server.vocab, ctx_server.mctx, query.get(), documents[i]); + auto tmp = format_prompt_rerank(ctx_server.model_tgt, ctx_server.vocab, ctx_server.mctx, query, documents[i]); server_task task = server_task(SERVER_TASK_TYPE_RERANK); task.id = rd.get_new_id(); task.tokens = std::move(tmp); @@ -5332,7 +5332,7 @@ std::unique_ptr server_routes::handle_embeddings_impl(cons bool use_base64 = false; if (body.count("encoding_format") != 0) { - const std::string format = body.at("encoding_format"); + const std::string & format = body.at("encoding_format"); if (format == "base64") { use_base64 = true; } else if (format != "float") { diff --git a/tools/server/server-http.cpp b/tools/server/server-http.cpp index 249e71b4167e..2ec137aa0786 100644 --- a/tools/server/server-http.cpp +++ b/tools/server/server-http.cpp @@ -785,7 +785,7 @@ void server_http_context::register_gcp_compat() const { try { json payload = instance; - const std::string format = payload.at("@requestFormat"); + const std::string format = payload.at("@requestFormat").get(); payload.erase("@requestFormat"); if (payload.contains("stream")) { diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 914c2b7021ae..7b368ef78810 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -306,7 +306,7 @@ std::vector> make_llama_cmpl_schema(const common_params & add((new field_str("generation_prompt")) ->set_desc("Generation prompt appended to the chat template output") ->set_handler([&](field_eval_context & ctx, const json & data) { - std::string s = data.at("generation_prompt"); + std::string s = data.at("generation_prompt").get(); ctx.params.chat_parser_params.generation_prompt = s; ctx.params.sampling.generation_prompt = s; })); @@ -399,13 +399,13 @@ std::vector> make_llama_cmpl_schema(const common_params & ctx.params.sampling.reasoning_budget_end.clear(); if (data.contains("reasoning_budget_end_tags")) { for (const auto & t : data.at("reasoning_budget_end_tags")) { - std::string tag = t; + std::string tag = t.get(); if (!tag.empty()) { ctx.params.sampling.reasoning_budget_end.push_back(common_tokenize(ctx.vocab, tag, false, true)); } } } else if (data.contains("reasoning_budget_end_tag")) { - std::string tag = data.at("reasoning_budget_end_tag"); + std::string tag = data.at("reasoning_budget_end_tag").get(); if (!tag.empty()) { ctx.params.sampling.reasoning_budget_end.push_back(common_tokenize(ctx.vocab, tag, false, true)); } diff --git a/tools/server/server-stream.cpp b/tools/server/server-stream.cpp index a591fa27f566..f6b9b8a9f4cc 100644 --- a/tools/server/server-stream.cpp +++ b/tools/server/server-stream.cpp @@ -510,7 +510,7 @@ server_http_context::handler_t server_stream_make_lookup_handler() { if (body.contains("conversation_ids") && body["conversation_ids"].is_array()) { for (const auto & v : body["conversation_ids"]) { if (v.is_string()) { - std::string id = v; + std::string id = v.get(); if (!id.empty()) { requested.push_back(std::move(id)); } diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index 82553fe01dd8..12e9dbb8cfa9 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -896,7 +896,7 @@ struct server_tool_read_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path"); + std::string path = params.at("path").get(); int start_line = json_value(params, "start_line", 1); int end_line = json_value(params, "end_line", -1); // -1 = no limit bool append_loc = json_value(params, "append_loc", false); @@ -1015,7 +1015,7 @@ struct server_tool_file_glob_search : server_tool { json invoke(json params, server_tool::stream *) const override { auto io = make_tools_io(params); - const std::string path = params.at("path"); + const std::string path = params.at("path").get(); std::string base = io->resolve(path); std::string include = json_value(params, "include", std::string("**")); @@ -1128,8 +1128,8 @@ struct server_tool_grep_search : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path"); - std::string pat_str = params.at("pattern"); + std::string path = params.at("path").get(); + std::string pat_str = params.at("pattern").get(); std::string include = json_value(params, "include", std::string("**")); std::string exclude = json_value(params, "exclude", std::string("")); bool show_lineno = json_value(params, "return_line_numbers", false); @@ -1271,7 +1271,7 @@ struct server_tool_exec_shell_command : server_tool { } json invoke(json params, server_tool::stream * st) const override { - std::string command = params.at("command"); + std::string command = params.at("command").get(); int timeout = json_value(params, "timeout", 10); size_t max_output = (size_t) json_value(params, "max_output_size", (int) SERVER_TOOL_EXEC_SHELL_COMMAND_MAX_OUTPUT_SIZE); @@ -1348,8 +1348,8 @@ struct server_tool_write_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path"); - std::string content = params.at("content"); + std::string path = params.at("path").get(); + std::string content = params.at("content").get(); auto io = make_tools_io(params); if (!io->write_file(path, content)) { @@ -1405,7 +1405,7 @@ struct server_tool_edit_file : server_tool { } json invoke(json params, server_tool::stream *) const override { - std::string path = params.at("path"); + std::string path = params.at("path").get(); const json & edits_json = params.at("edits"); if (!edits_json.is_array() || edits_json.empty()) { @@ -2078,7 +2078,7 @@ void server_tools::setup(const std::vector & enabled_tools, auto res = std::make_unique(); try { json body = json::parse(req.body); - std::string tool_name = body.at("tool"); + std::string tool_name = body.at("tool").get(); json params = body.value("params", json::object()); bool stream = body.value("stream", false); From 0c2f1190f40e8001421f804699613c5920ddd607 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Fri, 21 Aug 2026 23:11:29 +0200 Subject: [PATCH 09/19] wip --- common/chat.cpp | 26 +++++++++++++------------- common/download.cpp | 2 +- common/hf-cache.cpp | 8 ++++---- common/json.h | 9 ++++----- tests/test-chat-template.cpp | 4 ++-- tools/server/server-chat.cpp | 2 +- tools/server/server-mcp.cpp | 2 +- tools/server/server-tools.cpp | 4 ++-- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index 84f7ac8587d3..aa21aa14bb37 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -386,14 +386,14 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa if (!message.contains("role")) { throw std::invalid_argument("Missing 'role' in message: " + message.dump()); } - msg.role = message.at("role").get(); + msg.role = message.at("role"); auto has_content = message.contains("content"); auto has_tool_calls = message.contains("tool_calls"); if (has_content) { const auto & content = message.at("content"); if (content.is_string()) { - msg.content = content.get(); + msg.content = content; } else if (content.is_array()) { for (const auto & part : content) { if (!part.contains("type")) { @@ -404,8 +404,8 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa throw std::invalid_argument("Unsupported content part type: " + type.dump()); } common_chat_msg_content_part msg_part; - msg_part.type = type.get(); - msg_part.text = part.at("text").get(); + msg_part.type = type; + msg_part.text = part.at("text"); msg.content_parts.push_back(msg_part); } } else if (!content.is_null()) { @@ -431,15 +431,15 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa if (!fc.contains("name")) { throw std::invalid_argument("Missing tool call name: " + tool_call.dump()); } - tc.name = fc.at("name").get(); + tc.name = fc.at("name"); const auto & args = fc.at("arguments"); if (args.is_string()) { - tc.arguments = args.get(); + tc.arguments = args; } else { tc.arguments = args.dump(); } if (tool_call.contains("id")) { - tc.id = tool_call.at("id").get(); + tc.id = tool_call.at("id"); } msg.tool_calls.push_back(tc); } @@ -450,13 +450,13 @@ std::vector common_chat_msgs_parse_oaicompat(const json & messa "https://github.com/ggml-org/llama.cpp/issues/12279)"); } if (message.contains("reasoning_content")) { - msg.reasoning_content = message.at("reasoning_content").get(); + msg.reasoning_content = message.at("reasoning_content"); } if (message.contains("name")) { - msg.tool_name = message.at("name").get(); + msg.tool_name = message.at("name"); } if (message.contains("tool_call_id")) { - msg.tool_call_id = message.at("tool_call_id").get(); + msg.tool_call_id = message.at("tool_call_id"); } msgs.push_back(msg); @@ -937,7 +937,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::context ctx(tmpl.source()); // messages_override is already built for this template, do not touch its content parts - common_json inp = common_json{ + json inp = json{ {"messages", messages_override.has_value() ? *messages_override : messages_inp_normalizer(tmpl.original_caps()).normalize(inputs.messages)}, @@ -2482,7 +2482,7 @@ static common_chat_params common_chat_params_init_kimi_k3(const common_chat_temp std::string type = "string"; if (prop.value().is_object() && prop.value().contains("type") && prop.value().at("type").is_string()) { - type = prop.value().at("type").get(); + type = prop.value().at("type"); } auto value = type == "string" ? p.tool_arg_string_value(p.until(ARG_END)) : @@ -3799,7 +3799,7 @@ static common_chat_params common_chat_templates_apply_legacy(const struct common common_chat_params params; params.prompt = std::string(buf.data(), res); if (!inputs.json_schema.empty()) { - params.grammar = json_schema_to_grammar(common_json::parse(inputs.json_schema)); + params.grammar = json_schema_to_grammar(json::parse(inputs.json_schema)); } else { params.grammar = inputs.grammar; } diff --git a/common/download.cpp b/common/download.cpp index 4b28a708c86e..a7513974b013 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -924,7 +924,7 @@ std::string common_docker_resolve_model(const std::string & docker) { std::string media_type = layer["mediaType"].get(); if (media_type == "application/vnd.docker.ai.gguf.v3" || media_type.find("gguf") != std::string::npos) { - gguf_digest = layer["digest"].get(); + gguf_digest = layer["digest"]; break; } } diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index 50d6dd6105c4..55c4d3b9b389 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -213,7 +213,7 @@ static common_json api_get(const std::string & url, return common_json::parse(res->body); } try { - body = common_json::parse(res->body)["error"].get(); + body = common_json::parse(res->body)["error"]; } catch (...) { } throw std::runtime_error("GET failed (" + std::to_string(res->status) + "): " + body); @@ -320,7 +320,7 @@ hf_files get_repo_files(const std::string & repo_id, hf_file file; file.repo_id = repo_id; - file.path = item["path"].get(); + file.path = item["path"]; if (!is_valid_subpath(commit_path, file.path)) { LOG_WRN("%s: skip invalid path: %s\n", __func__, file.path.c_str()); @@ -329,10 +329,10 @@ hf_files get_repo_files(const std::string & repo_id, if (item.contains("lfs") && item["lfs"].is_object()) { if (item["lfs"].contains("oid") && item["lfs"]["oid"].is_string()) { - file.oid = item["lfs"]["oid"].get(); + file.oid = item["lfs"]["oid"]; } } else if (item.contains("oid") && item["oid"].is_string()) { - file.oid = item["oid"].get(); + file.oid = item["oid"]; } if (!file.oid.empty() && !is_valid_oid(file.oid)) { diff --git a/common/json.h b/common/json.h index 9415d57b3693..4ad517f3a729 100644 --- a/common/json.h +++ b/common/json.h @@ -170,12 +170,9 @@ class common_json { // implicit get() for plain values, so they can be assigned to their C++ type directly // note: kept to this short list on purpose, a wider one makes j["key"] ambiguous + // note: only std::string. adding a numeric one makes "str = json;" ambiguous, + // because a number can also convert to char, which std::string accepts operator std::string() const { return get(); } - operator bool() const { return get(); } - operator int() const { return get(); } - operator int64_t() const { return get(); } - operator float() const { return get(); } - operator double() const { return get(); } template T value(const std::string & key, T def) const { @@ -291,6 +288,8 @@ class common_json { private: // the backing value is built here, json.cpp checks that it fits + // it cannot be a pointer: a value inside a tree would then not be a common_json, + // so at() could only give back a copy instead of a real reference alignas(8) unsigned char storage[32]; }; diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index bcc574afe981..5255155af37e 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -299,10 +299,10 @@ void run_single(const std::string& contents, json input, bool use_common, bool d std::string bos_token = ""; std::string eos_token = ""; if (input.contains("bos_token")) { - bos_token = input["bos_token"].get(); + bos_token = input["bos_token"]; } if (input.contains("eos_token")) { - eos_token = input["eos_token"].get(); + eos_token = input["eos_token"]; } common_json msgs_json = input["messages"]; common_json tools_json = input["tools"]; diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index a6fe3c6ba619..17e4e2d09810 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -341,7 +341,7 @@ json server_chat_convert_anthropic_to_oai(const json & body) { std::string system_content; if (system_param.is_string()) { - system_content = system_param.get(); + system_content = system_param; normalize_anthropic_billing_header(system_content); } else if (system_param.is_array()) { for (const auto & block : system_param) { diff --git a/tools/server/server-mcp.cpp b/tools/server/server-mcp.cpp index 93db6164d34e..fcac5061c878 100644 --- a/tools/server/server-mcp.cpp +++ b/tools/server/server-mcp.cpp @@ -157,7 +157,7 @@ std::vector server_mcp_server_config::parse_cursor_for } if (cfg.contains("env") && cfg.at("env").is_object()) { for (const auto & [k, v] : cfg.at("env").items()) { - sc.env[k] = v.get(); + sc.env[k] = v; } } diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index 12e9dbb8cfa9..9861809d11cf 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -1420,8 +1420,8 @@ struct server_tool_edit_file : server_tool { edits.reserve(edits_json.size()); for (const auto & e : edits_json) { edit_req er; - er.old_text = e.at("old_text").get(); - er.new_text = e.at("new_text").get(); + er.old_text = e.at("old_text"); + er.new_text = e.at("new_text"); if (er.old_text.empty()) { return {{"error", string_format("edits[%zu].old_text must not be empty", edits.size())}}; } From 6609378e52d8a517f1edd777b02e67fa19a7d156 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 00:06:53 +0200 Subject: [PATCH 10/19] wip 2 --- common/chat.cpp | 1 + common/json-schema-to-grammar.cpp | 2 +- common/json.cpp | 28 ++++++++++++-------- common/json.h | 9 +++++-- tests/peg-parser/test-json-serialization.cpp | 4 +-- tests/peg-parser/tests.h | 8 +++--- tests/test-chat-peg-parser.cpp | 10 +++---- tests/test-jinja.cpp | 4 +-- tests/test-model-resolution.cpp | 2 +- 9 files changed, 40 insertions(+), 28 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index aa21aa14bb37..800c2cf1ddc6 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index bb1119cd7a2d..c7b63724bc9d 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -947,7 +947,7 @@ class common_schema_converter { } if ((schema_type.is_null() || schema_type == "object") && (schema.contains("properties") || - (schema.contains("additionalProperties") && !schema["additionalProperties"].get()))) { + (schema.contains("additionalProperties") && schema["additionalProperties"] != true))) { std::unordered_set required; if (schema.contains("required") && schema["required"].is_array()) { for (const auto & item : schema["required"]) { diff --git a/common/json.cpp b/common/json.cpp index dd0db5face41..8f9ca72fd39c 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -98,6 +98,7 @@ common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSO #define COMMON_JSON_VEC(...) template common_json_value::common_json_value(const std::vector<__VA_ARGS__> &); COMMON_JSON_VEC(int) +COMMON_JSON_VEC(unsigned char) COMMON_JSON_VEC(unsigned int) COMMON_JSON_VEC(long) COMMON_JSON_VEC(unsigned long) @@ -106,6 +107,7 @@ COMMON_JSON_VEC(unsigned long long) COMMON_JSON_VEC(float) COMMON_JSON_VEC(double) COMMON_JSON_VEC(std::string) +COMMON_JSON_VEC(std::vector) COMMON_JSON_VEC(common_json) #undef COMMON_JSON_VEC @@ -113,8 +115,10 @@ COMMON_JSON_VEC(common_json) common_json_value::common_json_value(std::initializer_list items) : type(VAL_JSON), val_json(std::make_shared(items)) {} +// null, same as the backing library. operator[] turns it into an object, +// push_back() into an array common_json::common_json() { - new (storage) ordered_json(ordered_json::object()); + new (storage) ordered_json(); } common_json::common_json(const common_json & other) { @@ -125,7 +129,9 @@ common_json::common_json(common_json && other) noexcept { new (storage) ordered_json(std::move(as_json(&other))); } -common_json::common_json(std::initializer_list items) : common_json() { +common_json::common_json(std::initializer_list items) { + new (storage) ordered_json(ordered_json::object()); + for (const auto & item : items) { set(item); } @@ -139,14 +145,8 @@ common_json::common_json(std::nullptr_t) { new (storage) ordered_json(nullptr); } -common_json & common_json::operator=(const common_json & other) { - as_json(this) = as_json(&other); - - return *this; -} - -common_json & common_json::operator=(common_json && other) noexcept { - as_json(this) = std::move(as_json(&other)); +common_json & common_json::operator=(common_json other) noexcept { + as_json(this).swap(as_json(&other)); return *this; } @@ -186,7 +186,7 @@ common_json common_json::array(std::initializer_list vals) { } common_json common_json::object() { - return common_json(); + return common_json_from_raw(ordered_json::object()); } common_json common_json::object(std::initializer_list items) { @@ -319,6 +319,11 @@ template T common_json::get() const { return as_json(this).get(); } +// the backing library cannot build a common_json, so this one is just a copy +template <> common_json common_json::get() const { + return *this; +} + // get() is usable only for the types below #define COMMON_JSON_GET(...) template __VA_ARGS__ common_json::get<__VA_ARGS__>() const; @@ -333,6 +338,7 @@ COMMON_JSON_GET(unsigned long long) COMMON_JSON_GET(float) COMMON_JSON_GET(double) COMMON_JSON_GET(std::string) +COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::set) COMMON_JSON_GET(std::vector) diff --git a/common/json.h b/common/json.h index 4ad517f3a729..f760c243edb5 100644 --- a/common/json.h +++ b/common/json.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,9 @@ struct common_json_value { common_json_value(std::nullptr_t = nullptr) : type(VAL_NULL) {} common_json_value(bool val) : type(VAL_BOOL), val_bool(val) {} common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} + // a string_view does not convert to std::string on its own, and without this + // it would land on the common_json ctor below and recurse + common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} common_json_value(const char * val); common_json_value(const common_json & val); // only for the types instantiated in json.cpp, the rest fails at link time @@ -102,8 +106,9 @@ class common_json { !std::is_same::type, common_json_value>::value, int>::type = 0> common_json(T && val) : common_json(common_json_value(std::forward(val))) {} - common_json & operator=(const common_json & other); - common_json & operator=(common_json && other) noexcept; + // by value, same as the backing library: the right side is copied before the + // left side can invalidate it, e.g. msg["a"] = msg.at("b") where "a" is new + common_json & operator=(common_json other) noexcept; ~common_json(); diff --git a/tests/peg-parser/test-json-serialization.cpp b/tests/peg-parser/test-json-serialization.cpp index a85801060c0b..da63a23bf218 100644 --- a/tests/peg-parser/test-json-serialization.cpp +++ b/tests/peg-parser/test-json-serialization.cpp @@ -8,7 +8,7 @@ void test_json_serialization(testing &t) { auto json_serialized = original.to_json().dump(); t.test("compare before/after", [&](testing &t) { - auto deserialized = common_peg_arena::from_json(nlohmann::json::parse(json_serialized)); + auto deserialized = common_peg_arena::from_json(common_json::parse(json_serialized)); // Test complex JSON std::string input = R"({"name": "test", "values": [1, 2, 3], "nested": {"a": true}})"; @@ -23,6 +23,6 @@ void test_json_serialization(testing &t) { }); t.bench("deserialize", [&]() { - auto deserialized = common_peg_arena::from_json(nlohmann::json::parse(json_serialized)); + auto deserialized = common_peg_arena::from_json(common_json::parse(json_serialized)); }, 100); } diff --git a/tests/peg-parser/tests.h b/tests/peg-parser/tests.h index debd4286c50a..00e81815b621 100644 --- a/tests/peg-parser/tests.h +++ b/tests/peg-parser/tests.h @@ -1,7 +1,7 @@ #pragma once // Common includes for all test files -#include +#include "json.h" #include #include @@ -11,9 +11,9 @@ #include "simple-tokenize.h" struct bench_tool_call { - std::string id; - std::string name; - nlohmann::ordered_json args; + std::string id; + std::string name; + common_json args; }; // Test function declarations diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp index cede556cf34a..793891394ce6 100644 --- a/tests/test-chat-peg-parser.cpp +++ b/tests/test-chat-peg-parser.cpp @@ -63,10 +63,10 @@ static json create_tools() { { { "type", "string" }, { "description", "The city and state, e.g. San Francisco, CA" } } }, { "unit", { { "type", "string" }, - { "enum", { "celsius", "fahrenheit" } }, + { "enum", json::array({ "celsius", "fahrenheit" }) }, { "description", "The temperature unit to use. Infer this from the users location." } } } } }, - { "required", { "location", "unit" } }, + { "required", json::array({ "location", "unit" }) }, } }, } } }; @@ -86,14 +86,14 @@ static json create_tools() { { { "type", "string" }, { "description", "The city and state, e.g. San Francisco, CA" } } }, { "unit", { { "type", "string" }, - { "enum", { "celsius", "fahrenheit" } }, + { "enum", json::array({ "celsius", "fahrenheit" }) }, { "description", "The temperature unit to use. Infer this from the users location." } } }, { "days", { { "type", "integer" }, { "description", "Number of days to forecast (1-10)" }, { "minimum", 1 }, { "maximum", 10 } } } } }, - { "required", { "location", "unit" } }, + { "required", json::array({ "location", "unit" }) }, } }, } } }; @@ -341,7 +341,7 @@ static void test_example_native(testing & t) { { { "invoice_number", { { "type", "string" } } }, { "amount", { { "type", "number" } } }, { "due_date", { { "type", "string" } } } } }, - { "required", { "invoice_number", "amount", "due_date" } } }, + { "required", json::array({ "invoice_number", "amount", "due_date" }) } }, /* .parallel_tool_calls = */ false, /* .generation_prompt = */ "", /* .input = */ diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 4b68d5538306..974a3f9dd8df 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -240,7 +240,7 @@ static void test_conditionals(testing & t) { test_template(t, "is undefined key falsy", "{{ 'yes' if not y['x'] else 'no' }}", - {{"y", {{}}}}, + {{"y", json::array({nullptr})}}, "yes" ); @@ -282,7 +282,7 @@ static void test_conditionals(testing & t) { test_template(t, "is non-empty object truthy", "{{ 'yes' if y else 'no' }}", - {{"y", {"x", false}}}, + {{"y", json::array({"x", false})}}, "yes" ); diff --git a/tests/test-model-resolution.cpp b/tests/test-model-resolution.cpp index 80b02fb70a1c..5191e77514ab 100644 --- a/tests/test-model-resolution.cpp +++ b/tests/test-model-resolution.cpp @@ -55,7 +55,7 @@ static const char * COMMIT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; static void serve_repos(httplib::Server & server) { server.Get(R"(/api/models/(.+)/refs)", [](const httplib::Request & req, httplib::Response & res) { if (g_repos.count(req.matches[1])) { - res.set_content(common_json{{"branches", {{{"name", "main"}, {"targetCommit", COMMIT}}}}}.dump(), + res.set_content(common_json{{"branches", common_json::array({ common_json{{"name", "main"}, {"targetCommit", COMMIT}} })}}.dump(), "application/json"); } else { res.status = 404; From 34861194be060e5e848882cd04bf9c1d9cb187d9 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 00:48:33 +0200 Subject: [PATCH 11/19] revert redundant changes --- common/arg.cpp | 15 ++++++++------- common/chat-diff-analyzer.cpp | 6 +++--- common/chat-peg-parser.cpp | 1 - common/chat.cpp | 5 ++--- common/download.cpp | 2 +- common/hf-cache.cpp | 8 ++++---- common/jinja/caps.cpp | 2 +- common/jinja/value.cpp | 17 ++++++++--------- common/json-schema-to-grammar.cpp | 10 +++++----- common/json-schema-to-grammar.h | 1 - common/json.cpp | 4 ++++ common/json.h | 3 +++ tests/test-chat-template.cpp | 4 ++-- tools/server/server-chat.cpp | 2 +- tools/server/server-chat.h | 1 - tools/server/server-common.cpp | 21 ++++++++------------- tools/server/server-context.cpp | 1 - tools/server/server-mcp.cpp | 2 +- tools/server/server-schema.cpp | 2 +- tools/server/server-task.cpp | 2 -- tools/server/server-tools.cpp | 4 ++-- 21 files changed, 54 insertions(+), 59 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 4a36ff9b0ccc..9896f2ceff32 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -54,6 +54,7 @@ #define LLAMA_MAX_URL_LENGTH 2084 // Maximum URL Length in Chrome: 2083 +using json = common_json; using namespace common_arg_utils; static std::initializer_list mmproj_examples = { @@ -2270,7 +2271,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"-j", "--json-schema"}, "SCHEMA", "JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object\nFor schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead", [](common_params & params, const std::string & value) { - params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(common_json::parse(value))}; + params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(value))}; } ).set_sampling()); add_opt(common_arg( @@ -2287,7 +2288,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex std::istreambuf_iterator(), std::back_inserter(schema) ); - params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(common_json::parse(schema))}; + params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(schema))}; } ).set_sampling()); add_opt(common_arg( @@ -3498,13 +3499,13 @@ common_params_context common_params_parser_init(common_params & params, llama_ex {"--chat-template-kwargs"}, "STRING", "sets additional params for the json template parser, must be a valid json object string, e.g. '{\"key1\":\"value1\",\"key2\":\"value2\"}'", [](common_params & params, const std::string & value) { - auto parsed = common_json::parse(value); - for (const auto & [key, val] : parsed.items()) { - if (key == "enable_thinking") { + auto parsed = json::parse(value); + for (const auto & item : parsed.items()) { + if (item.key() == "enable_thinking") { LOG_WRN("Setting 'enable_thinking' via --chat-template-kwargs is deprecated. " "Use --reasoning on / --reasoning off instead.\n"); } - params.default_template_kwargs[key] = val.dump(); + params.default_template_kwargs[item.key()] = item.value().dump(); } } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_CHAT_TEMPLATE_KWARGS")); @@ -3672,7 +3673,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex if (value == "default") { params.default_template_kwargs.erase("reasoning_effort"); } else { - params.default_template_kwargs["reasoning_effort"] = common_json::make(value).dump(); + params.default_template_kwargs["reasoning_effort"] = json(value).dump(); } } ).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_COMPLETION, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_REASONING_EFFORT")); diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp index 79ce8153bf67..a7e370578fd9 100644 --- a/common/chat-diff-analyzer.cpp +++ b/common/chat-diff-analyzer.cpp @@ -6,9 +6,9 @@ #include "log.h" #include "peg-parser.h" -#include #include #include +#include #include #include @@ -930,9 +930,9 @@ void analyze_tools::analyze_tool_call_format_json_native(const std::string & cle std::string cut = clean_haystack.substr(json_start, json_end - json_start + 1); json call_struct = json::parse(cut); auto register_field = [&](const std::string & prefix, const common_json_entry & subel) { - if (subel.value().is_string() && subel.value().get().find("call0000") != std::string::npos) { + if (subel.value().is_string() && std::string(subel.value()).find("call0000") != std::string::npos) { format.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key(); - } else if (subel.value().is_string() && subel.value().get() == fun_name_needle) { + } else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) { format.name_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key(); } else if (subel.value().dump().find(arg_name_needle) != std::string::npos) { // handle both string and JSON obj variants diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index e17134b6c2d3..79b97a80f1b2 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -4,7 +4,6 @@ #include "ggml.h" #include "peg-parser.h" - #include #include diff --git a/common/chat.cpp b/common/chat.cpp index 800c2cf1ddc6..24618d35aee5 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -14,7 +14,6 @@ #include "jinja/caps.h" #include "peg-parser.h" - #include #include #include @@ -596,7 +595,7 @@ std::vector common_chat_tools_parse_oaicompat(const json & too const auto & function = tool.at("function"); result.push_back({ - /* .name = */ function.at("name").get(), + /* .name = */ function.at("name"), /* .description = */ function.value("description", ""), /* .parameters = */ function.value("parameters", json::object()).dump(), }); @@ -2483,7 +2482,7 @@ static common_chat_params common_chat_params_init_kimi_k3(const common_chat_temp std::string type = "string"; if (prop.value().is_object() && prop.value().contains("type") && prop.value().at("type").is_string()) { - type = prop.value().at("type"); + type = prop.value().at("type").get(); } auto value = type == "string" ? p.tool_arg_string_value(p.until(ARG_END)) : diff --git a/common/download.cpp b/common/download.cpp index a7513974b013..4b28a708c86e 100644 --- a/common/download.cpp +++ b/common/download.cpp @@ -924,7 +924,7 @@ std::string common_docker_resolve_model(const std::string & docker) { std::string media_type = layer["mediaType"].get(); if (media_type == "application/vnd.docker.ai.gguf.v3" || media_type.find("gguf") != std::string::npos) { - gguf_digest = layer["digest"]; + gguf_digest = layer["digest"].get(); break; } } diff --git a/common/hf-cache.cpp b/common/hf-cache.cpp index 55c4d3b9b389..50d6dd6105c4 100644 --- a/common/hf-cache.cpp +++ b/common/hf-cache.cpp @@ -213,7 +213,7 @@ static common_json api_get(const std::string & url, return common_json::parse(res->body); } try { - body = common_json::parse(res->body)["error"]; + body = common_json::parse(res->body)["error"].get(); } catch (...) { } throw std::runtime_error("GET failed (" + std::to_string(res->status) + "): " + body); @@ -320,7 +320,7 @@ hf_files get_repo_files(const std::string & repo_id, hf_file file; file.repo_id = repo_id; - file.path = item["path"]; + file.path = item["path"].get(); if (!is_valid_subpath(commit_path, file.path)) { LOG_WRN("%s: skip invalid path: %s\n", __func__, file.path.c_str()); @@ -329,10 +329,10 @@ hf_files get_repo_files(const std::string & repo_id, if (item.contains("lfs") && item["lfs"].is_object()) { if (item["lfs"].contains("oid") && item["lfs"]["oid"].is_string()) { - file.oid = item["lfs"]["oid"]; + file.oid = item["lfs"]["oid"].get(); } } else if (item.contains("oid") && item["oid"].is_string()) { - file.oid = item["oid"]; + file.oid = item["oid"].get(); } if (!file.oid.empty() && !is_valid_oid(file.oid)) { diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 53965d990335..9971c021e188 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -370,7 +370,7 @@ caps caps_get(jinja::program & prog) { caps_try_execute( prog, [&]() { - json args = json::make(R"({"arg": "value"})"); + json args = json(R"({"arg": "value"})"); if (result.supports_object_arguments) { args = json{{"arg", "value"}}; } diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 1af09d6628cd..6999ef7d6706 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -1378,8 +1378,8 @@ static value from_json(const common_json & j, bool mark_input) { return arr; } else if (j.is_object()) { auto obj = mk_val(); - for (const auto & [key, val] : j.items()) { - obj->insert(key, from_json(val, mark_input)); + for (auto it = j.begin(); it != j.end(); ++it) { + obj->insert(it.key(), from_json(it.value(), mark_input)); } return obj; } else { @@ -1451,19 +1451,18 @@ bool value_compare(const value & a, const value & b, value_compare_op op) { return result; } -template -void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input) { +template<> +void global_from_json(context & ctx, const common_json & json_obj, bool mark_input) { + // printf("global_from_json: %s\n" , json_obj.dump(2).c_str()); if (json_obj.is_null() || !json_obj.is_object()) { throw std::runtime_error("global_from_json: input JSON value must be an object"); } - for (const auto & [key, val] : json_obj.items()) { - JJ_DEBUG("global_from_json: setting key '%s'", key.c_str()); - ctx.set_val(key, from_json(val, mark_input)); + for (auto it = json_obj.begin(); it != json_obj.end(); ++it) { + JJ_DEBUG("global_from_json: setting key '%s'", it.key().c_str()); + ctx.set_val(it.key(), from_json(it.value(), mark_input)); } } -template void global_from_json(context &, const common_json &, bool); - // recursively convert value to JSON string // TODO: avoid circular references static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) { diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index c7b63724bc9d..2c873905d502 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -916,7 +916,7 @@ class common_schema_converter { std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name; if (schema.contains("$ref")) { - return _add_rule(rule_name, _resolve_ref(schema["$ref"].get())); + return _add_rule(rule_name, _resolve_ref(schema["$ref"])); } if (schema.contains("oneOf") || schema.contains("anyOf")) { const json & alts = schema.contains("oneOf") ? schema.at("oneOf") : schema.at("anyOf"); @@ -974,7 +974,7 @@ class common_schema_converter { const std::string& hybrid_name = name; std::function add_component = [&](const json & comp_schema, bool is_required) { if (comp_schema.contains("$ref")) { - add_component(_refs[comp_schema["$ref"].get()], is_required); + add_component(_refs[comp_schema["$ref"]], is_required); } else if (comp_schema.contains("properties")) { for (const auto & prop : comp_schema["properties"].items()) { properties.emplace_back(prop.key(), prop.value()); @@ -1037,7 +1037,7 @@ class common_schema_converter { return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " space \"]\""); } if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) { - return _visit_pattern(schema["pattern"].get(), rule_name); + return _visit_pattern(schema["pattern"], rule_name); } if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) { return _add_primitive(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid")); @@ -1135,7 +1135,7 @@ bool common_schema_info::resolves_to_string(const common_json & schema) { // Handle $ref if (s.contains("$ref")) { - const std::string ref = s["$ref"]; + const std::string & ref = s["$ref"]; if (visited_refs.find(ref) != visited_refs.end()) { // Circular reference, assume not a string to be safe return false; @@ -1218,7 +1218,7 @@ bool common_schema_info::resolves_to_string(const common_json & schema) { // Check format - many formats imply string if (s.contains("format")) { - const std::string fmt = s["format"]; + const std::string & fmt = s["format"]; if (fmt == "date" || fmt == "time" || fmt == "date-time" || fmt == "uri" || fmt == "email" || fmt == "hostname" || fmt == "ipv4" || fmt == "ipv6" || fmt == "uuid" || diff --git a/common/json-schema-to-grammar.h b/common/json-schema-to-grammar.h index cc84d4f6cddc..84ed71c76a13 100644 --- a/common/json-schema-to-grammar.h +++ b/common/json-schema-to-grammar.h @@ -2,7 +2,6 @@ #include "json.h" - #include #include #include diff --git a/common/json.cpp b/common/json.cpp index 8f9ca72fd39c..a40622437be8 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -82,6 +82,10 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} +common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON) { + val_json = std::make_shared(common_json_from_raw(ordered_json(vals))); +} + template common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { common_json out = common_json::array(); diff --git a/common/json.h b/common/json.h index f760c243edb5..bc4b65b031df 100644 --- a/common/json.h +++ b/common/json.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,8 @@ struct common_json_value { common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} common_json_value(const char * val); common_json_value(const common_json & val); + // becomes an object, so a plain string map can be passed where a JSON value is expected + common_json_value(const std::map & vals); // only for the types instantiated in json.cpp, the rest fails at link time template common_json_value(const std::vector & vals); diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp index 5255155af37e..bcc574afe981 100644 --- a/tests/test-chat-template.cpp +++ b/tests/test-chat-template.cpp @@ -299,10 +299,10 @@ void run_single(const std::string& contents, json input, bool use_common, bool d std::string bos_token = ""; std::string eos_token = ""; if (input.contains("bos_token")) { - bos_token = input["bos_token"]; + bos_token = input["bos_token"].get(); } if (input.contains("eos_token")) { - eos_token = input["eos_token"]; + eos_token = input["eos_token"].get(); } common_json msgs_json = input["messages"]; common_json tools_json = input["tools"]; diff --git a/tools/server/server-chat.cpp b/tools/server/server-chat.cpp index 17e4e2d09810..a6fe3c6ba619 100644 --- a/tools/server/server-chat.cpp +++ b/tools/server/server-chat.cpp @@ -341,7 +341,7 @@ json server_chat_convert_anthropic_to_oai(const json & body) { std::string system_content; if (system_param.is_string()) { - system_content = system_param; + system_content = system_param.get(); normalize_anthropic_billing_header(system_content); } else if (system_param.is_array()) { for (const auto & block : system_param) { diff --git a/tools/server/server-chat.h b/tools/server/server-chat.h index 2b4945980c95..86b842650ea3 100644 --- a/tools/server/server-chat.h +++ b/tools/server/server-chat.h @@ -8,7 +8,6 @@ #include "json.h" - // Convert OpenAI Responses API format to OpenAI Chat Completions API format json server_chat_convert_responses_to_chatcmpl(const json & body); diff --git a/tools/server/server-common.cpp b/tools/server/server-common.cpp index 8111a3ba1079..4f5b8202aca5 100644 --- a/tools/server/server-common.cpp +++ b/tools/server/server-common.cpp @@ -9,7 +9,6 @@ #include "server-common.h" - #include #include #include @@ -978,9 +977,9 @@ static server_tokens tokenize_input_subprompt(const llama_vocab * vocab, mtmd_co // JSON object with prompt and multimodal key. std::vector files; for (const auto & entry : json_prompt.at(JSON_MTMD_DATA_KEY)) { - files.push_back(base64_decode(entry.get())); + files.push_back(base64_decode(entry)); } - return process_mtmd_prompt(mctx, json_prompt.at(JSON_STRING_PROMPT_KEY).get(), files); + return process_mtmd_prompt(mctx, json_prompt.at(JSON_STRING_PROMPT_KEY), files); } else { // Not multimodal, but contains a subobject. llama_tokens tmp = tokenize_mixed(vocab, json_prompt.at(JSON_STRING_PROMPT_KEY), add_special, parse_special); @@ -1301,8 +1300,7 @@ json oaicompat_chat_params_parse( } // parse the "enable_thinking" kwarg to override the default value - const auto kwarg_it = inputs.chat_template_kwargs.find("enable_thinking"); - std::string enable_thinking_kwarg = kwarg_it == inputs.chat_template_kwargs.end() ? "" : kwarg_it->second; + auto enable_thinking_kwarg = json_value(inputs.chat_template_kwargs, "enable_thinking", std::string("")); if (enable_thinking_kwarg == "true") { inputs.enable_thinking = true; } else if (enable_thinking_kwarg == "false") { @@ -1318,7 +1316,7 @@ json oaicompat_chat_params_parse( inputs.enable_thinking = false; inputs.chat_template_kwargs.erase("reasoning_effort"); } else if (!reasoning_effort.empty()) { - inputs.chat_template_kwargs["reasoning_effort"] = json::make(reasoning_effort).dump(); + inputs.chat_template_kwargs["reasoning_effort"] = json(reasoning_effort).dump(); } } @@ -1467,10 +1465,7 @@ json format_response_rerank( }); elements.resize(std::min(top_n, (int)elements.size())); - json results = json::array(); - for (const auto & el : elements) { - results.push_back(el); - } + json results = elements; if (is_tei_format) return results; @@ -1795,12 +1790,12 @@ server_tokens format_prompt_rerank( std::string prompt = rerank_prompt; string_replace_all(prompt, "{query}" , query); string_replace_all(prompt, "{document}", doc ); - server_tokens tokens = tokenize_input_subprompt(vocab, mctx, json::make(prompt), false, true); + server_tokens tokens = tokenize_input_subprompt(vocab, mctx, prompt, false, true); result.push_back(tokens); } else { // Get EOS token - use SEP token as fallback if EOS is not available - server_tokens query_tokens = tokenize_input_subprompt(vocab, mctx, json::make(query), false, false); - server_tokens doc_tokens = tokenize_input_subprompt(vocab, mctx, json::make(doc), false, false); + server_tokens query_tokens = tokenize_input_subprompt(vocab, mctx, query, false, false); + server_tokens doc_tokens = tokenize_input_subprompt(vocab, mctx, doc, false, false); llama_token eos_token = llama_vocab_eos(vocab); if (eos_token == LLAMA_TOKEN_NULL) { eos_token = llama_vocab_sep(vocab); diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index 744ffccd3a52..c47c063b76fe 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -35,7 +35,6 @@ #include #endif - constexpr int HTTP_POLLING_SECONDS = 1; static common_speculative_output_limits server_output_limits(const common_params & params) { diff --git a/tools/server/server-mcp.cpp b/tools/server/server-mcp.cpp index fcac5061c878..93db6164d34e 100644 --- a/tools/server/server-mcp.cpp +++ b/tools/server/server-mcp.cpp @@ -157,7 +157,7 @@ std::vector server_mcp_server_config::parse_cursor_for } if (cfg.contains("env") && cfg.at("env").is_object()) { for (const auto & [k, v] : cfg.at("env").items()) { - sc.env[k] = v; + sc.env[k] = v.get(); } } diff --git a/tools/server/server-schema.cpp b/tools/server/server-schema.cpp index 7b368ef78810..64b9251295ce 100644 --- a/tools/server/server-schema.cpp +++ b/tools/server/server-schema.cpp @@ -487,7 +487,7 @@ std::vector> make_llama_cmpl_schema(const common_params & const auto & stop = data.at("stop"); if (stop.is_array()) { for (const auto & word : stop) { - if (!word.empty()) ctx.params.antiprompt.push_back(word.get()); + if (!word.empty()) ctx.params.antiprompt.push_back(word); } } else if (stop.is_string()) { ctx.params.antiprompt.push_back(stop.get()); diff --git a/tools/server/server-task.cpp b/tools/server/server-task.cpp index 2bca3dd74408..0d3beb313cea 100644 --- a/tools/server/server-task.cpp +++ b/tools/server/server-task.cpp @@ -1,6 +1,5 @@ #include "server-task.h" - #include "build-info.h" #include "server-chat.h" #include "chat.h" @@ -13,7 +12,6 @@ #include - // // task_params // diff --git a/tools/server/server-tools.cpp b/tools/server/server-tools.cpp index 9861809d11cf..12e9dbb8cfa9 100644 --- a/tools/server/server-tools.cpp +++ b/tools/server/server-tools.cpp @@ -1420,8 +1420,8 @@ struct server_tool_edit_file : server_tool { edits.reserve(edits_json.size()); for (const auto & e : edits_json) { edit_req er; - er.old_text = e.at("old_text"); - er.new_text = e.at("new_text"); + er.old_text = e.at("old_text").get(); + er.new_text = e.at("new_text").get(); if (er.old_text.empty()) { return {{"error", string_format("edits[%zu].old_text must not be empty", edits.size())}}; } From b99247112efab51f68f44084a268d55f1fabaf1e Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 01:07:12 +0200 Subject: [PATCH 12/19] fix server crash --- common/json.cpp | 38 ++++++++++++++++++++++++++++++++++++-- common/json.h | 32 +++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/common/json.cpp b/common/json.cpp index a40622437be8..40cff037c308 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -82,10 +82,44 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} -common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON) { - val_json = std::make_shared(common_json_from_raw(ordered_json(vals))); +template +common_json_value::common_json_value(const std::set & vals) : type(VAL_JSON) { + common_json out = common_json::array(); + + for (const auto & val : vals) { + out.push_back(val); + } + + val_json = std::make_shared(std::move(out)); +} + +// a set value is usable only for the types below +#define COMMON_JSON_SET(...) template common_json_value::common_json_value(const std::set<__VA_ARGS__> &); + +COMMON_JSON_SET(int) +COMMON_JSON_SET(std::string) + +#undef COMMON_JSON_SET + +template +common_json_value::common_json_value(const std::map & vals) : type(VAL_JSON) { + common_json out = common_json::object(); + + for (const auto & val : vals) { + out.set({ val.first, val.second }); + } + + val_json = std::make_shared(std::move(out)); } +// a map value is usable only for the types below +#define COMMON_JSON_MAP(...) template common_json_value::common_json_value(const std::map &); + +COMMON_JSON_MAP(bool) +COMMON_JSON_MAP(std::string) + +#undef COMMON_JSON_MAP + template common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { common_json out = common_json::array(); diff --git a/common/json.h b/common/json.h index bc4b65b031df..6f009b2a55f8 100644 --- a/common/json.h +++ b/common/json.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -59,10 +60,12 @@ struct common_json_value { common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} common_json_value(const char * val); common_json_value(const common_json & val); - // becomes an object, so a plain string map can be passed where a JSON value is expected - common_json_value(const std::map & vals); // only for the types instantiated in json.cpp, the rest fails at link time template common_json_value(const std::vector & vals); + // a set becomes an array, in the set's own order + template common_json_value(const std::set & vals); + // a map becomes an object, keyed in the map's own order + template common_json_value(const std::map & vals); // nested object, e.g. {"fn", {{"name", "x"}}} common_json_value(std::initializer_list items); @@ -93,6 +96,26 @@ struct common_json_item { key(std::move(key)), val(items) {} }; +// the types common_json_value holds on its own. anything else reaches its +// common_json ctor, which builds a common_json again and never stops +template struct common_json_is_value : std::integral_constant::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value || + std::is_same::value> {}; + +template +struct common_json_is_value> : std::true_type {}; + +template +struct common_json_is_value> : std::true_type {}; + +template +struct common_json_is_value> : std::true_type {}; + class common_json { public: common_json(); @@ -107,7 +130,10 @@ class common_json { // one step, so that "abc" or a vector can go straight into a common_json template ::type, common_json>::value && !std::is_same::type, common_json_value>::value, int>::type = 0> - common_json(T && val) : common_json(common_json_value(std::forward(val))) {} + common_json(T && val) : common_json(common_json_value(std::forward(val))) { + static_assert(common_json_is_value::type>::value, + "no common_json_value ctor holds this type, add one instead of letting it recurse"); + } // by value, same as the backing library: the right side is copied before the // left side can invalidate it, e.g. msg["a"] = msg.at("b") where "a" is new From e8bdf2cb5255965498e251530307f179d0b3bab8 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 01:37:12 +0200 Subject: [PATCH 13/19] various fixes --- common/json-schema-to-grammar.cpp | 4 -- common/json.cpp | 74 ++++++++++++++++++++----------- common/json.h | 9 +++- tools/cli/cli-context.cpp | 1 - tools/server/server-common.h | 1 - 5 files changed, 57 insertions(+), 32 deletions(-) diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 2c873905d502..37c89e6b8492 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -1,10 +1,6 @@ #include "json-schema-to-grammar.h" -// the grammar builder walks the schema with the library API -#include "json-shim.h" #include "common.h" -#include - #include #include #include diff --git a/common/json.cpp b/common/json.cpp index 40cff037c308..046294892f6a 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -18,6 +18,16 @@ using nlohmann::ordered_json; static_assert(sizeof(ordered_json) <= sizeof(common_json), "common_json storage is too small"); static_assert(alignof(ordered_json) <= alignof(common_json), "common_json alignment is too weak"); +// runs fn and gives every error of the backing library as a common_json_error +template +static decltype(auto) guard(F && fn) { + try { + return fn(); + } catch (const ordered_json::exception & e) { + throw common_json_error(e.what()); + } +} + static ordered_json & as_json(common_json * self) { return *reinterpret_cast(self); } @@ -42,7 +52,12 @@ static ordered_json to_json(const common_json_value & val) { case common_json_value::VAL_UINT: return val.val_uint; case common_json_value::VAL_DOUBLE: return val.val_double; case common_json_value::VAL_STRING: return val.val_string; - case common_json_value::VAL_JSON: return as_json(val.val_json.get()); + case common_json_value::VAL_JSON: + // one owner means no one else can see this tree, so it is safe to move it out + if (val.val_json.use_count() == 1) { + return std::move(as_json(val.val_json.get())); + } + return as_json(val.val_json.get()); } return nullptr; @@ -82,6 +97,9 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} +common_json_value::common_json_value(common_json && val) : + type(VAL_JSON), val_json(std::make_shared(std::move(val))) {} + template common_json_value::common_json_value(const std::set & vals) : type(VAL_JSON) { common_json out = common_json::array(); @@ -259,15 +277,15 @@ bool common_json::operator!=(const common_json_value & val) const { return !(*this == val); } -common_json & common_json::at(const std::string & key) { return as_common(as_json(this).at(key)); } -const common_json & common_json::at(const std::string & key) const { return as_common(as_json(this).at(key)); } -common_json & common_json::at(size_t idx) { return as_common(as_json(this).at(idx)); } -const common_json & common_json::at(size_t idx) const { return as_common(as_json(this).at(idx)); } +common_json & common_json::at(const std::string & key) { return guard([&]() -> common_json & { return as_common(as_json(this).at(key)); }); } +const common_json & common_json::at(const std::string & key) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(key)); }); } +common_json & common_json::at(size_t idx) { return guard([&]() -> common_json & { return as_common(as_json(this).at(idx)); }); } +const common_json & common_json::at(size_t idx) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(idx)); }); } -common_json & common_json::operator[](const std::string & key) { return as_common(as_json(this)[key]); } -const common_json & common_json::operator[](const std::string & key) const { return as_common(as_json(this).at(key)); } -common_json & common_json::operator[](size_t idx) { return as_common(as_json(this)[idx]); } -const common_json & common_json::operator[](size_t idx) const { return as_common(as_json(this).at(idx)); } +common_json & common_json::operator[](const std::string & key) { return guard([&]() -> common_json & { return as_common(as_json(this)[key]); }); } +const common_json & common_json::operator[](const std::string & key) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(key)); }); } +common_json & common_json::operator[](size_t idx) { return guard([&]() -> common_json & { return as_common(as_json(this)[idx]); }); } +const common_json & common_json::operator[](size_t idx) const { return guard([&]() -> const common_json & { return as_common(as_json(this).at(idx)); }); } common_json & common_json::front() { return as_common(as_json(this).front()); } const common_json & common_json::front() const { return as_common(as_json(this).front()); } @@ -279,11 +297,11 @@ void common_json::clear() { } void common_json::erase(const std::string & key) { - as_json(this).erase(key); + guard([&] { as_json(this).erase(key); }); } void common_json::erase(size_t idx) { - as_json(this).erase(idx); + guard([&] { as_json(this).erase(idx); }); } void common_json::assign(const common_json_value & val) { @@ -291,17 +309,17 @@ void common_json::assign(const common_json_value & val) { } void common_json::set(const common_json_item & item) { - as_json(this)[item.key] = to_json(item.val); + guard([&] { as_json(this)[item.key] = to_json(item.val); }); } void common_json::push_back(const common_json_value & val) { - as_json(this).push_back(to_json(val)); + guard([&] { as_json(this).push_back(to_json(val)); }); } void common_json::push_back(std::initializer_list items) { common_json val(items); - as_json(this).push_back(as_json(&val)); + guard([&] { as_json(this).push_back(std::move(as_json(&val))); }); } size_t common_json::count(const std::string & key) const { @@ -309,13 +327,15 @@ size_t common_json::count(const std::string & key) const { } void common_json::insert(const common_json & vals) { - ordered_json & self = as_json(this); + guard([&] { + ordered_json & self = as_json(this); - self.insert(self.end(), as_json(&vals).begin(), as_json(&vals).end()); + self.insert(self.end(), as_json(&vals).begin(), as_json(&vals).end()); + }); } std::string common_json::dump(int indent) const { - return as_json(this).dump(indent); + return guard([&] { return as_json(this).dump(indent); }); } std::string common_json::dump_safe(int indent) const { @@ -324,15 +344,17 @@ std::string common_json::dump_safe(int indent) const { // an array is indexed directly, an object needs a walk from the start common_json & common_json::iterator::operator*() const { - if (as_json(node).is_object()) { - return as_common(std::next(as_json(node).begin(), idx).value()); - } + return guard([&]() -> common_json & { + if (as_json(node).is_object()) { + return as_common(std::next(as_json(node).begin(), idx).value()); + } - return as_common(as_json(node)[idx]); + return as_common(as_json(node)[idx]); + }); } std::string common_json::iterator::key() const { - return std::next(as_json(node).begin(), idx).key(); + return guard([&] { return std::next(as_json(node).begin(), idx).key(); }); } common_json::iterator common_json::begin() const { @@ -344,9 +366,11 @@ common_json::iterator common_json::end() const { } common_json::items_view::entry common_json::items_view::iterator::operator*() const { - auto it = std::next(as_json(node).begin(), idx); + return guard([&]() -> entry { + auto it = std::next(as_json(node).begin(), idx); - return { it.key(), as_common(it.value()) }; + return { it.key(), as_common(it.value()) }; + }); } common_json::items_view common_json::items() const { @@ -354,7 +378,7 @@ common_json::items_view common_json::items() const { } template T common_json::get() const { - return as_json(this).get(); + return guard([&] { return as_json(this).get(); }); } // the backing library cannot build a common_json, so this one is just a copy diff --git a/common/json.h b/common/json.h index 6f009b2a55f8..6f2d0841a164 100644 --- a/common/json.h +++ b/common/json.h @@ -4,6 +4,7 @@ // the underlay library is pimpl, it should never be exposed here // the backing value lives inside this object, so at() and the iterators give a real reference to it // note: object keys keep the order in which they are added +// note: every JSON error comes out as a common_json_error #include #include @@ -60,6 +61,7 @@ struct common_json_value { common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} common_json_value(const char * val); common_json_value(const common_json & val); + common_json_value(common_json && val); // only for the types instantiated in json.cpp, the rest fails at link time template common_json_value(const std::vector & vals); // a set becomes an array, in the set's own order @@ -68,6 +70,8 @@ struct common_json_value { template common_json_value(const std::map & vals); // nested object, e.g. {"fn", {{"name", "x"}}} + // note: a nested pair {"a", "b"} becomes the object {"a": "b"}, not an array + // use common_json::array({"a", "b"}) to get an array common_json_value(std::initializer_list items); template ::value && !std::is_same::value, int>::type = 0> @@ -174,7 +178,7 @@ class common_json { bool operator==(const common_json_value & val) const; bool operator!=(const common_json_value & val) const; - // at() throws if the key is missing, operator[] adds a null value instead + // at() throws common_json_error if the key is missing, operator[] adds a null value instead common_json & at(const std::string & key); const common_json & at(const std::string & key) const; common_json & at(size_t idx); @@ -327,4 +331,7 @@ class common_json { alignas(8) unsigned char storage[32]; }; +// json.cpp defines this specialization, it must be declared before any use of it +template <> common_json common_json::get() const; + using common_json_entry = common_json::items_view::entry; diff --git a/tools/cli/cli-context.cpp b/tools/cli/cli-context.cpp index f6a9c74ac620..aa4eb76796d9 100644 --- a/tools/cli/cli-context.cpp +++ b/tools/cli/cli-context.cpp @@ -6,7 +6,6 @@ #include "log.h" #include "console.h" -#define JSON_ASSERT GGML_ASSERT #include "json.h" #include diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 48dbdfcb6306..6478d0218cea 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -6,7 +6,6 @@ #include "chat.h" #include "mtmd.h" -#define JSON_ASSERT GGML_ASSERT #include "json.h" #include From cdcd63bc038caeb7485c79d088a774dcd86da620 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 02:04:35 +0200 Subject: [PATCH 14/19] fix ci --- common/json-schema-to-grammar.cpp | 1 + tools/server/server-context.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 37c89e6b8492..0aee51b26e84 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -2,6 +2,7 @@ #include "common.h" #include +#include #include #include #include diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp index c47c063b76fe..d7a6b887d868 100644 --- a/tools/server/server-context.cpp +++ b/tools/server/server-context.cpp @@ -655,14 +655,14 @@ struct server_slot { res["n_prompt_tokens_processed"] = stats.n_prompt_processed; res["n_prompt_tokens_cache"] = stats.n_prompt_cached; res["params"] = ptask->params.to_json(only_metrics); - res["next_token"] = { + res["next_token"] = json::array({ { {"has_next_token", has_next_token}, {"has_new_line", has_new_line}, {"n_remain", n_remaining()}, {"n_decoded", stats.n_gen}, } - }; + }); if (!only_metrics) { res["prompt"] = ptask->tokens.detokenize(ctx_tgt, true); From b89d589040b000f0cd5901c8f97fb307eedffc61 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 10:08:39 +0200 Subject: [PATCH 15/19] harden a bit --- common/json.cpp | 56 ++++++++++++++++++++++++++++-------- common/json.h | 17 +++++++++-- common/peg-parser.cpp | 7 +++-- tools/server/server-common.h | 2 +- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/common/json.cpp b/common/json.cpp index 046294892f6a..62abc38ba1ec 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -54,6 +54,7 @@ static ordered_json to_json(const common_json_value & val) { case common_json_value::VAL_STRING: return val.val_string; case common_json_value::VAL_JSON: // one owner means no one else can see this tree, so it is safe to move it out + // note: this makes a value single use, same as the json_ref of the backing library if (val.val_json.use_count() == 1) { return std::move(as_json(val.val_json.get())); } @@ -213,14 +214,19 @@ common_json::~common_json() { common_json common_json::parse(const std::string & text) { try { - return common_json_from_raw(ordered_json::parse(text)); + // the assignment moves the parsed tree in, it does not copy + common_json out; + as_json(&out) = ordered_json::parse(text); + return out; } catch (const std::exception & e) { throw common_json_error(e.what()); } } common_json common_json::parse_no_throw(const std::string & text) { - return common_json_from_raw(ordered_json::parse(text, nullptr, false)); + common_json out; + as_json(&out) = ordered_json::parse(text, nullptr, false); + return out; } bool common_json::is_discarded() const { @@ -228,21 +234,27 @@ bool common_json::is_discarded() const { } common_json common_json::array() { - return common_json_from_raw(ordered_json::array()); + common_json out; + as_json(&out) = ordered_json::array(); + return out; } common_json common_json::array(std::initializer_list vals) { - ordered_json out = ordered_json::array(); + common_json out; + ordered_json & arr = as_json(&out); + arr = ordered_json::array(); for (const auto & val : vals) { - out.push_back(to_json(val)); + arr.push_back(to_json(val)); } - return common_json_from_raw(out); + return out; } common_json common_json::object() { - return common_json_from_raw(ordered_json::object()); + common_json out; + as_json(&out) = ordered_json::object(); + return out; } common_json common_json::object(std::initializer_list items) { @@ -270,6 +282,10 @@ bool common_json::contains(const std::string & key) const { } bool common_json::operator==(const common_json_value & val) const { + // compare a tree in place, to_json() would copy it + if (val.type == common_json_value::VAL_JSON) { + return as_json(this) == as_json(val.val_json.get()); + } return as_json(this) == to_json(val); } @@ -345,11 +361,17 @@ std::string common_json::dump_safe(int indent) const { // an array is indexed directly, an object needs a walk from the start common_json & common_json::iterator::operator*() const { return guard([&]() -> common_json & { - if (as_json(node).is_object()) { - return as_common(std::next(as_json(node).begin(), idx).value()); + ordered_json & j = as_json(node); + + if (j.is_object()) { + return as_common(std::next(j.begin(), idx).value()); + } + if (j.is_array()) { + return as_common(j[idx]); } - return as_common(as_json(node)[idx]); + // a plain value gives itself once, same as the backing library + return *node; }); } @@ -365,11 +387,21 @@ common_json::iterator common_json::end() const { return iterator(const_cast(this), size()); } +// the keys follow the backing library: the index for an array, "" for a plain value common_json::items_view::entry common_json::items_view::iterator::operator*() const { return guard([&]() -> entry { - auto it = std::next(as_json(node).begin(), idx); + ordered_json & j = as_json(node); + + if (j.is_object()) { + auto it = std::next(j.begin(), idx); + + return { it.key(), as_common(it.value()) }; + } + if (j.is_array()) { + return { std::to_string(idx), as_common(j[idx]) }; + } - return { it.key(), as_common(it.value()) }; + return { std::string(), *node }; }); } diff --git a/common/json.h b/common/json.h index 6f2d0841a164..aa2913dfbcc3 100644 --- a/common/json.h +++ b/common/json.h @@ -30,6 +30,8 @@ struct common_json_error : std::runtime_error { }; // one value, tagged so that this header stays free of the backing library +// note: a value that holds a tree is single use, consuming the same value twice +// (e.g. through a named initializer list) gives null on the second use struct common_json_value { enum value_type { VAL_NULL, @@ -188,8 +190,8 @@ class common_json { const common_json & operator[](const std::string & key) const; common_json & operator[](const char * key) { return (*this)[std::string(key)]; } const common_json & operator[](const char * key) const { return (*this)[std::string(key)]; } - common_json & operator[](int idx) { return (*this)[(size_t) idx]; } - const common_json & operator[](int idx) const { return (*this)[(size_t) idx]; } + common_json & operator[](int idx) { return (*this)[to_idx(idx)]; } + const common_json & operator[](int idx) const { return (*this)[to_idx(idx)]; } common_json & operator[](size_t idx); const common_json & operator[](size_t idx) const; @@ -236,7 +238,7 @@ class common_json { // 1 if the key is there, 0 if not size_t count(const std::string & key) const; - // appends every value of another array + // appends every value of another array; inserting an array into itself throws void insert(const common_json & vals); // a common_json goes through the copy assignment above, everything else becomes a value @@ -252,6 +254,7 @@ class common_json { std::string dump_safe(int indent = -1) const; // walks an array by index, or an object in insertion order + // a plain value gives itself once, same as the backing library class iterator { public: using iterator_category = std::forward_iterator_tag; @@ -325,6 +328,14 @@ class common_json { items_view items() const; private: + // a negative index must not turn into a huge size_t + static size_t to_idx(int idx) { + if (idx < 0) { + throw common_json_error("negative array index"); + } + return (size_t) idx; + } + // the backing value is built here, json.cpp checks that it fits // it cannot be a pointer: a value inside a tree would then not be a common_json, // so at() could only give back a copy instead of a real reference diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 96cc267b9d9b..8194ddb1d8db 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -1895,11 +1895,14 @@ common_json common_peg_arena::to_json() const { for (const auto & parser : parsers_) { parsers.push_back(serialize_parser_variant(parser)); } - return common_json_from_raw(nlohmann::ordered_json{ + // the assignment moves the tree in, it does not copy + common_json out; + common_json_raw(out) = nlohmann::ordered_json{ {"parsers", parsers}, {"rules", rules_}, {"root", root_} - }); + }; + return out; } static common_peg_parser_variant deserialize_parser_variant(const nlohmann::ordered_json & j) { diff --git a/tools/server/server-common.h b/tools/server/server-common.h index 6478d0218cea..f8ea82ef4cf5 100644 --- a/tools/server/server-common.h +++ b/tools/server/server-common.h @@ -42,7 +42,7 @@ static T json_value(const json & body, const std::string & key, const T & defaul if (body.contains(key) && !body.at(key).is_null()) { try { return body.at(key).get(); - } catch (const std::exception & err) { + } catch (const common_json_error & err) { LOG_WRN("Wrong type supplied for parameter '%s', using default value: %s\n", key.c_str(), err.what()); return default_value; } From ad32502d1441af5807655a86e2576e19832b7dc4 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 10:28:04 +0200 Subject: [PATCH 16/19] clean up --- common/json-shim.h | 4 ++-- common/json.cpp | 4 ++-- common/json.h | 21 +++++++++------------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/common/json-shim.h b/common/json-shim.h index 9c8cda0fe789..1a1d201d37a7 100644 --- a/common/json-shim.h +++ b/common/json-shim.h @@ -2,8 +2,8 @@ // converts between common_json and the backing JSON library // -// include this only in a cpp file that touches an internal component of the library, -// never in a header. every use here is a place to fix if the library changes. +// include this only in a cpp file that touches an internal component, never in a header +// every use here is a place to fix if the library changes #include "json.h" diff --git a/common/json.cpp b/common/json.cpp index 62abc38ba1ec..6c2be95d66f9 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -172,8 +172,8 @@ COMMON_JSON_VEC(common_json) common_json_value::common_json_value(std::initializer_list items) : type(VAL_JSON), val_json(std::make_shared(items)) {} -// null, same as the backing library. operator[] turns it into an object, -// push_back() into an array +// null, same as the backing library +// operator[] turns it into an object, push_back() into an array common_json::common_json() { new (storage) ordered_json(); } diff --git a/common/json.h b/common/json.h index aa2913dfbcc3..a16bbad1eede 100644 --- a/common/json.h +++ b/common/json.h @@ -30,8 +30,7 @@ struct common_json_error : std::runtime_error { }; // one value, tagged so that this header stays free of the backing library -// note: a value that holds a tree is single use, consuming the same value twice -// (e.g. through a named initializer list) gives null on the second use +// note: a value that holds a tree is single use, the second use gives null struct common_json_value { enum value_type { VAL_NULL, @@ -58,8 +57,7 @@ struct common_json_value { common_json_value(std::nullptr_t = nullptr) : type(VAL_NULL) {} common_json_value(bool val) : type(VAL_BOOL), val_bool(val) {} common_json_value(std::string val) : type(VAL_STRING), val_string(std::move(val)) {} - // a string_view does not convert to std::string on its own, and without this - // it would land on the common_json ctor below and recurse + // without this a string_view lands on the common_json ctor below and recurses common_json_value(std::string_view val) : type(VAL_STRING), val_string(val) {} common_json_value(const char * val); common_json_value(const common_json & val); @@ -102,8 +100,8 @@ struct common_json_item { key(std::move(key)), val(items) {} }; -// the types common_json_value holds on its own. anything else reaches its -// common_json ctor, which builds a common_json again and never stops +// the types common_json_value holds on its own +// anything else reaches its common_json ctor and recurses forever template struct common_json_is_value : std::integral_constant::value || std::is_same::value || @@ -141,8 +139,8 @@ class common_json { "no common_json_value ctor holds this type, add one instead of letting it recurse"); } - // by value, same as the backing library: the right side is copied before the - // left side can invalidate it, e.g. msg["a"] = msg.at("b") where "a" is new + // by value, same as the backing library + // the right side is copied before the left side can invalidate it, e.g. msg["a"] = msg.at("b") common_json & operator=(common_json other) noexcept; ~common_json(); @@ -210,8 +208,7 @@ class common_json { // implicit get() for plain values, so they can be assigned to their C++ type directly // note: kept to this short list on purpose, a wider one makes j["key"] ambiguous - // note: only std::string. adding a numeric one makes "str = json;" ambiguous, - // because a number can also convert to char, which std::string accepts + // note: a numeric one would make "str = json;" ambiguous, a number converts to char too operator std::string() const { return get(); } template @@ -337,8 +334,8 @@ class common_json { } // the backing value is built here, json.cpp checks that it fits - // it cannot be a pointer: a value inside a tree would then not be a common_json, - // so at() could only give back a copy instead of a real reference + // it cannot be a pointer: a value inside a tree would then not be a common_json + // at() could then only give back a copy instead of a real reference alignas(8) unsigned char storage[32]; }; From 527b42a575268f03779842f60153d21971265fd3 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 10:35:31 +0200 Subject: [PATCH 17/19] rm json-shim --- common/CMakeLists.txt | 1 - common/json-shim.h | 16 --------------- common/json.cpp | 45 ++++++++++++++++++++----------------------- common/json.h | 5 +++++ common/peg-parser.cpp | 29 +++++++++++----------------- 5 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 common/json-shim.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 3f7d4b760d37..36f1e0cd50f1 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -81,7 +81,6 @@ add_library(${TARGET} imatrix-loader.cpp imatrix-loader.h json-schema-to-grammar.cpp - json-shim.h json.cpp json.h llguidance.cpp diff --git a/common/json-shim.h b/common/json-shim.h deleted file mode 100644 index 1a1d201d37a7..000000000000 --- a/common/json-shim.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -// converts between common_json and the backing JSON library -// -// include this only in a cpp file that touches an internal component, never in a header -// every use here is a place to fix if the library changes - -#include "json.h" - -template T & common_json_raw(common_json & json); -template const T & common_json_raw(const common_json & json); - -template common_json common_json_from_raw(const T & json); - -// view over a value of the backing library, it does not copy -template common_json & common_json_ref_from_raw(T & json); diff --git a/common/json.cpp b/common/json.cpp index 6c2be95d66f9..547542bb7d74 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -1,6 +1,4 @@ #include "json.h" -// defines the shim -#include "json-shim.h" #include "ggml.h" @@ -10,6 +8,7 @@ #include #include #include +#include #include using nlohmann::ordered_json; @@ -64,28 +63,6 @@ static ordered_json to_json(const common_json_value & val) { return nullptr; } -template T & common_json_raw(common_json & json) { - return as_json(&json); -} - -template const T & common_json_raw(const common_json & json) { - return as_json(&json); -} - -template common_json common_json_from_raw(const T & json) { - return common_json(as_common(json)); -} - -template common_json & common_json_ref_from_raw(T & json) { - return as_common(json); -} - -// the bridge is usable only for the type below -template ordered_json & common_json_raw(common_json &); -template const ordered_json & common_json_raw(const common_json &); -template common_json common_json_from_raw(const ordered_json &); -template common_json & common_json_ref_from_raw(ordered_json &); - common_json_value::common_json_value(const char * val) { if (val) { type = VAL_STRING; @@ -139,6 +116,24 @@ COMMON_JSON_MAP(std::string) #undef COMMON_JSON_MAP +template +common_json_value::common_json_value(const std::unordered_map & vals) : type(VAL_JSON) { + common_json out = common_json::object(); + + for (const auto & val : vals) { + out.set({ val.first, val.second }); + } + + val_json = std::make_shared(std::move(out)); +} + +// an unordered map value is usable only for the types below +#define COMMON_JSON_UMAP(...) template common_json_value::common_json_value(const std::unordered_map &); + +COMMON_JSON_UMAP(size_t) + +#undef COMMON_JSON_UMAP + template common_json_value::common_json_value(const std::vector & vals) : type(VAL_JSON) { common_json out = common_json::array(); @@ -436,5 +431,7 @@ COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::vector) COMMON_JSON_GET(std::set) COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::vector) +COMMON_JSON_GET(std::unordered_map) #undef COMMON_JSON_GET diff --git a/common/json.h b/common/json.h index a16bbad1eede..8bef391d43e4 100644 --- a/common/json.h +++ b/common/json.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -68,6 +69,7 @@ struct common_json_value { template common_json_value(const std::set & vals); // a map becomes an object, keyed in the map's own order template common_json_value(const std::map & vals); + template common_json_value(const std::unordered_map & vals); // nested object, e.g. {"fn", {{"name", "x"}}} // note: a nested pair {"a", "b"} becomes the object {"a": "b"}, not an array @@ -120,6 +122,9 @@ struct common_json_is_value> : std::true_type {}; template struct common_json_is_value> : std::true_type {}; +template +struct common_json_is_value> : std::true_type {}; + class common_json { public: common_json(); diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 8194ddb1d8db..46fc29bf2f8b 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -1,6 +1,4 @@ #include "peg-parser.h" -// the interface takes common_json, the parser internals stay on the library -#include "json-shim.h" #include "common.h" #include "json-schema-to-grammar.h" @@ -12,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -1807,8 +1804,8 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo } } -static nlohmann::ordered_json serialize_parser_variant(const common_peg_parser_variant & variant) { - using json = nlohmann::ordered_json; +static common_json serialize_parser_variant(const common_peg_parser_variant & variant) { + using json = common_json; return std::visit([](const auto & p) -> json { using T = std::decay_t; @@ -1862,7 +1859,7 @@ static nlohmann::ordered_json serialize_parser_variant(const common_peg_parser_v {"type", "schema"}, {"child", p.child}, {"name", p.name}, - {"schema", p.schema ? common_json_raw(*p.schema) : nlohmann::ordered_json(nullptr)}, + {"schema", p.schema ? *p.schema : json(nullptr)}, {"raw", p.raw} }; } else if constexpr (std::is_same_v) { @@ -1891,21 +1888,18 @@ static nlohmann::ordered_json serialize_parser_variant(const common_peg_parser_v } common_json common_peg_arena::to_json() const { - auto parsers = nlohmann::ordered_json::array(); + auto parsers = common_json::array(); for (const auto & parser : parsers_) { parsers.push_back(serialize_parser_variant(parser)); } - // the assignment moves the tree in, it does not copy - common_json out; - common_json_raw(out) = nlohmann::ordered_json{ + return common_json{ {"parsers", parsers}, {"rules", rules_}, {"root", root_} }; - return out; } -static common_peg_parser_variant deserialize_parser_variant(const nlohmann::ordered_json & j) { +static common_peg_parser_variant deserialize_parser_variant(const common_json & j) { if (!j.contains("type") || !j["type"].is_string()) { throw std::runtime_error("Parser variant JSON missing or invalid 'type' field"); } @@ -1974,9 +1968,9 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::orde } common_peg_chars_parser parser; parser.pattern = j["pattern"]; - parser.negated = j["negated"]; - parser.min_count = j["min_count"]; - parser.max_count = j["max_count"]; + parser.negated = j["negated"].get(); + parser.min_count = j["min_count"].get(); + parser.max_count = j["max_count"].get(); for (const auto & range_json : j["ranges"]) { if (!range_json.contains("start") || !range_json.contains("end")) { throw std::runtime_error("char_range missing 'start' or 'end' field"); @@ -2012,7 +2006,7 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::orde parser.child = j["child"].get(); parser.name = j["name"]; if (!j["schema"].is_null()) { - parser.schema = std::make_shared(common_json_from_raw(j["schema"])); + parser.schema = std::make_shared(j["schema"]); } parser.raw = j["raw"].get(); return parser; @@ -2074,8 +2068,7 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::orde throw std::runtime_error("Unknown parser type: " + type); } -common_peg_arena common_peg_arena::from_json(const common_json & j_in) { - const nlohmann::ordered_json & j = common_json_raw(j_in); +common_peg_arena common_peg_arena::from_json(const common_json & j) { if (!j.contains("parsers") || !j["parsers"].is_array()) { throw std::runtime_error("JSON missing or invalid 'parsers' array"); } From 3c7135fc4de5716a71aeeafb6e696ed57744ed12 Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 15:20:35 +0200 Subject: [PATCH 18/19] add some comments --- common/json.h | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/common/json.h b/common/json.h index 8bef391d43e4..98b6fc3db6e5 100644 --- a/common/json.h +++ b/common/json.h @@ -1,11 +1,5 @@ #pragma once -// JSON object, it works without the need to include a JSON library header -// the underlay library is pimpl, it should never be exposed here -// the backing value lives inside this object, so at() and the iterators give a real reference to it -// note: object keys keep the order in which they are added -// note: every JSON error comes out as a common_json_error - #include #include #include @@ -21,6 +15,19 @@ #include #include +// common_json, a thin wrapper around vendor json library +// the underlay library is pimpl, we are using nlohmann::json for now +// +// many features of the library are deliberately left out, to keep this interface small and generic and to keep compile time down +// +// some main differences compared to nlohmann::json : +// - object keys keep the order in which they are added +// - errors are always throw as common_json_error +// - obj.push_back({key, val}) is intentionally unsupported to avoid confusion with push_back on a vector; write it as obj[key] = val for clarity +// - a braced pair in value position does not build, e.g. {"key", {"a", "b"}}; write array({"a", "b"}) where nlohmann made an array +// +// in doubt, search the code base for an existing usage example; do not add anything to this header unless absolutely necessary + class common_json; // common_json_value holds a list of these, and each of them holds a value, so one must come first @@ -72,8 +79,7 @@ struct common_json_value { template common_json_value(const std::unordered_map & vals); // nested object, e.g. {"fn", {{"name", "x"}}} - // note: a nested pair {"a", "b"} becomes the object {"a": "b"}, not an array - // use common_json::array({"a", "b"}) to get an array + // note: a nested pair {"a", "b"} does not build, use common_json::array({"a", "b"}) for an array common_json_value(std::initializer_list items); template ::value && !std::is_same::value, int>::type = 0> @@ -184,6 +190,7 @@ class common_json { bool operator!=(const common_json_value & val) const; // at() throws common_json_error if the key is missing, operator[] adds a null value instead + // note: a const operator[] cannot add, it throws like at() common_json & at(const std::string & key); const common_json & at(const std::string & key) const; common_json & at(size_t idx); From ff0b1959a44c4cb7e2fc1f45dd709d45f5b54b8f Mon Sep 17 00:00:00 2001 From: Xuan Son Nguyen Date: Sat, 22 Aug 2026 15:22:57 +0200 Subject: [PATCH 19/19] rm redundant decl --- common/json.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/json.h b/common/json.h index 98b6fc3db6e5..9e20a2adb2f0 100644 --- a/common/json.h +++ b/common/json.h @@ -351,7 +351,4 @@ class common_json { alignas(8) unsigned char storage[32]; }; -// json.cpp defines this specialization, it must be declared before any use of it -template <> common_json common_json::get() const; - using common_json_entry = common_json::items_view::entry;