common: add json.h abstraction (#27511)

* add common/json

* migrate common

* adapt jinja

* migrate server

* big wip

* migrate tests

* wip

* revert some excessive changes

* wip

* wip 2

* revert redundant changes

* fix server crash

* various fixes

* fix ci

* harden a bit

* clean up

* rm json-shim

* add some comments

* rm redundant decl
This commit is contained in:
Xuan-Son Nguyen
2026-08-22 16:28:28 +02:00
committed by GitHub
parent 2fb989b9e7
commit d9f918d2d0
45 changed files with 987 additions and 217 deletions
+5 -6
View File
@@ -6,8 +6,7 @@
#include "log.h"
#include "console.h"
#define JSON_ASSERT GGML_ASSERT
#include <nlohmann/json.hpp>
#include "json.h"
#include <algorithm>
#include <cctype>
@@ -16,7 +15,7 @@
#include <map>
#include <set>
using json = nlohmann::ordered_json;
using json = common_json;
struct cli_context_impl {
json messages = json::array();
@@ -73,7 +72,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 +156,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 +363,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;
}
+2 -2
View File
@@ -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 <fstream>
@@ -15,7 +15,7 @@
#include <sstream>
#include <string>
using json = nlohmann::ordered_json;
using json = common_json;
enum class output_mode {
ANALYSIS, // Only output analysis results (default)
+2 -2
View File
@@ -11,9 +11,9 @@
#include <vector>
#include <algorithm>
#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"
+1 -1
View File
@@ -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");
+1 -3
View File
@@ -6,9 +6,7 @@
#include "server-common.h"
#include "server-http.h"
#include <nlohmann/json_fwd.hpp>
using json = nlohmann::ordered_json;
#include "json.h"
// Convert OpenAI Responses API format to OpenAI Chat Completions API format
json server_chat_convert_responses_to_chatcmpl(const json & body);
+1 -1
View File
@@ -1540,7 +1540,7 @@ std::vector<llama_token_data> 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
+5 -6
View File
@@ -6,8 +6,7 @@
#include "chat.h"
#include "mtmd.h"
#define JSON_ASSERT GGML_ASSERT
#include <nlohmann/json.hpp>
#include "json.h"
#include <atomic>
#include <chrono>
@@ -19,7 +18,7 @@
#include <string>
#include <vector>
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 +41,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<T>();
} 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;
}
} else {
+14 -15
View File
@@ -35,8 +35,6 @@
#include <windows.h>
#endif
using json = nlohmann::ordered_json;
constexpr int HTTP_POLLING_SECONDS = 1;
static common_speculative_output_limits server_output_limits(const common_params & params) {
@@ -657,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);
@@ -4165,7 +4163,8 @@ std::unique_ptr<server_res_generator> 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++) {
@@ -4428,8 +4427,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},
@@ -4438,23 +4437,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),
}}
})}
};
}
@@ -4990,7 +4989,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<llama_tokens>();
content = tokens_to_str(ctx_server.vocab, tokens);
}
@@ -5297,7 +5296,7 @@ std::unique_ptr<server_res_generator> 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<int>();
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);
}
+1 -1
View File
@@ -4,7 +4,7 @@
#include "server-task.h"
#include "server-queue.h"
#include <nlohmann/json_fwd.hpp>
#include "json.h"
#include <cstddef>
#include <memory>
+1 -1
View File
@@ -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);
+2 -3
View File
@@ -503,7 +503,7 @@ std::vector<std::unique_ptr<field>> 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<std::vector<std::string>>());
} else if (samplers.is_string()) {
ctx.params.sampling.samplers = common_sampler_types_from_chars(samplers.get<std::string>());
}
@@ -580,8 +580,7 @@ static void handle_with_catch(const char * name, std::function<void()> 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 <typename T>
+13 -15
View File
@@ -12,8 +12,6 @@
#include <sstream>
using json = nlohmann::ordered_json;
//
// task_params
//
@@ -304,7 +302,7 @@ json completion_token_output::probs_vector_to_json(const std::vector<completion_
}
float completion_token_output::logarithm(float x) {
// nlohmann::json converts -inf to null, so we need to prevent that
// the JSON library converts -inf to null, so we need to prevent that
return x == 0.0f ? std::numeric_limits<float>::lowest() : std::log(x);
}
@@ -407,7 +405,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 +453,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 +514,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 +707,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 +1059,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 +1099,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 +1153,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 +1303,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();
}
}
-1
View File
@@ -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,
+1 -1
View File
@@ -2156,7 +2156,7 @@ void server_tools::setup(const std::vector<std::string> & 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) {