mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-13 17:52:59 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a89937354 | ||
|
|
37b3a9e0cc | ||
|
|
002a12ad25 | ||
|
|
f1e44dcc11 | ||
|
|
56b9eb280a | ||
|
|
790cf51aab | ||
|
|
8e330954ad | ||
|
|
acecd56032 | ||
|
|
ae9afff8d2 | ||
|
|
737e0980fe | ||
|
|
3057bb66c8 | ||
|
|
56381e407c | ||
|
|
c8edceb061 | ||
|
|
e192abb406 | ||
|
|
718f7b4175 |
@@ -221,7 +221,6 @@ jobs:
|
||||
# 7z x "-o${env:RUNNER_TEMP}" $env:RUNNER_TEMP/sde.tar
|
||||
# $sde = $(join-path $env:RUNNER_TEMP sde-external-${env:SDE_VERSION}-win/sde.exe)
|
||||
# cd build
|
||||
# $env:LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR = 1
|
||||
# & $sde -future -- ctest -L main -C Release --verbose --timeout 900
|
||||
|
||||
- name: ccache-clear
|
||||
|
||||
@@ -116,7 +116,6 @@ jobs:
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
cd tools/server/tests
|
||||
export ${{ matrix.extra_args }}
|
||||
PYTEST_WORKERS=1 ./tests.sh
|
||||
|
||||
- name: Slow tests
|
||||
@@ -125,5 +124,4 @@ jobs:
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
cd tools/server/tests
|
||||
export ${{ matrix.extra_args }}
|
||||
PYTEST_WORKERS=1 SLOW_TESTS=1 ./tests.sh
|
||||
|
||||
@@ -197,6 +197,16 @@ llama_option_depr(WARNING LLAMA_CURL)
|
||||
include("cmake/license.cmake")
|
||||
license_add_file("llama.cpp" "LICENSE")
|
||||
|
||||
#
|
||||
# compile options
|
||||
#
|
||||
|
||||
# clang stores the modification time of the precompiled header sources inside the
|
||||
# header and rejects it when they differ, so the timestamp is left out of it
|
||||
add_compile_options(
|
||||
"$<$<COMPILE_LANG_AND_ID:C,Clang,IntelLLVM>:SHELL:-Xclang -fno-pch-timestamp>"
|
||||
"$<$<COMPILE_LANG_AND_ID:CXX,Clang,IntelLLVM>:SHELL:-Xclang -fno-pch-timestamp>")
|
||||
|
||||
#
|
||||
# 3rd-party
|
||||
#
|
||||
|
||||
@@ -775,7 +775,11 @@ function gg_run_test_backend_ops {
|
||||
|
||||
set -e
|
||||
|
||||
local args_extra="-j $(nproc)"
|
||||
local n_jobs=$(nproc)
|
||||
if [ "${n_jobs}" -gt 2 ]; then
|
||||
n_jobs=2
|
||||
fi
|
||||
local args_extra="-j ${n_jobs}"
|
||||
|
||||
# TODO: fix multi-threaded for ROCm
|
||||
# https://github.com/ggml-org/llama.cpp/actions/runs/34576278519/job/103297889044?pr=28740#step:3:4865
|
||||
@@ -789,10 +793,11 @@ function gg_run_test_backend_ops {
|
||||
args_extra=""
|
||||
fi
|
||||
|
||||
# TODO: reduce the test-backend-ops timeout to 1800s
|
||||
if [ ! -z ${GG_BUILD_HIGH_PERF} ]; then
|
||||
(time ./bin/test-backend-ops ${args_extra} -b CPU) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log
|
||||
(time timeout 3600 ./bin/test-backend-ops ${args_extra} -b CPU) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log
|
||||
else
|
||||
(time ./bin/test-backend-ops ${args_extra} ) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log
|
||||
(time timeout 3600 ./bin/test-backend-ops ${args_extra} ) 2>&1 | tee -a $OUT/${ci}-test-backend-ops.log
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
@@ -84,6 +84,8 @@ add_library(${TARGET}
|
||||
imatrix-loader.cpp
|
||||
imatrix-loader.h
|
||||
json-schema-to-grammar.cpp
|
||||
json-schema.cpp
|
||||
json-schema.h
|
||||
json.cpp
|
||||
json.h
|
||||
llguidance.cpp
|
||||
|
||||
+3
-3
@@ -2277,14 +2277,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
).set_sampling());
|
||||
add_opt(common_arg(
|
||||
{"-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",
|
||||
"JSON schema to constrain generations (https://json-schema.org/), e.g. `{\"type\": \"object\"}` for any JSON object",
|
||||
[](common_params & params, const std::string & value) {
|
||||
params.sampling.grammar = {COMMON_GRAMMAR_TYPE_OUTPUT_FORMAT, json_schema_to_grammar(json::parse(value))};
|
||||
}
|
||||
).set_sampling());
|
||||
add_opt(common_arg(
|
||||
{"-jf", "--json-schema-file"}, "FILE",
|
||||
"File containing a 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",
|
||||
"File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{\"type\": \"object\"}` for any JSON object",
|
||||
[](common_params & params, const std::string & value) {
|
||||
std::ifstream file(value);
|
||||
if (!file) {
|
||||
@@ -3875,7 +3875,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
|
||||
{"--no-log-jsonl"},
|
||||
"Log as JSONL (one JSON object per line) to stdout, this also disables colored logging (default: disabled)",
|
||||
[](common_params &, bool value) {
|
||||
common_log_set_jsonl(common_log_main(), value);
|
||||
common_log_set_jsonl(value);
|
||||
}
|
||||
).set_env("LLAMA_ARG_LOG_JSONL"));
|
||||
add_opt(common_arg(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "common.h"
|
||||
#include "json-schema-to-grammar.h"
|
||||
#include "log.h"
|
||||
#include "parsers/parsers.h"
|
||||
#include "peg-parser.h"
|
||||
|
||||
#include <stdexcept>
|
||||
@@ -12,16 +13,6 @@
|
||||
|
||||
using json = common_json;
|
||||
|
||||
// Helper to iterate over tools/functions
|
||||
static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) {
|
||||
for (const auto & tool : tools) {
|
||||
if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) {
|
||||
continue;
|
||||
}
|
||||
fn(tool);
|
||||
}
|
||||
}
|
||||
|
||||
namespace autoparser {
|
||||
|
||||
parser_build_context::parser_build_context(common_chat_peg_builder & p, const generation_params & inputs) :
|
||||
@@ -87,15 +78,6 @@ common_chat_params peg_generator::generate_parser(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
@@ -312,7 +294,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");
|
||||
const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object();
|
||||
const auto schema = common_chat_tool_parameters(func);
|
||||
|
||||
// Build call_id parser based on position (if supported)
|
||||
bool have_call_id = false;
|
||||
@@ -383,43 +365,31 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
|
||||
common_peg_parser tool_choice = p.choice();
|
||||
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & func = tool.at("function");
|
||||
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();
|
||||
|
||||
std::set<std::string> required;
|
||||
if (params.contains("required")) {
|
||||
required = params.at("required").get<std::set<std::string>>();
|
||||
}
|
||||
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
const auto & func = tool.at("function");
|
||||
std::string name = func.at("name");
|
||||
|
||||
// Build parser for each argument, separating required and optional
|
||||
std::vector<common_peg_parser> required_parsers;
|
||||
std::vector<common_peg_parser> optional_parsers;
|
||||
for (const auto & [param_name, param_schema] : properties.items()) {
|
||||
bool is_required = required.find(param_name) != required.end();
|
||||
|
||||
foreach_parameter(func, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
|
||||
auto arg =
|
||||
p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param_name)) +
|
||||
p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param.name)) +
|
||||
arguments.name_suffix) +
|
||||
arguments.value_prefix +
|
||||
(schema_info.resolves_to_string(param_schema) ?
|
||||
(param.schema->may_be_string() ?
|
||||
p.ac(p.tool_arg_string_value(until_suffix) +
|
||||
p.tool_arg_close(p.literal(arguments.value_suffix)), arguments.value_suffix) :
|
||||
(p.tool_arg_json_value(p.schema(
|
||||
p.json(), "tool-" + name + "-arg-" + param_name + "-schema", param_schema, false)) +
|
||||
p.json(), "tool-" + name + "-arg-" + param.name + "-schema", doc, *param.schema)) +
|
||||
p.tool_arg_close(p.literal(arguments.value_suffix)))));
|
||||
|
||||
auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg);
|
||||
if (is_required) {
|
||||
auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg);
|
||||
if (param.required) {
|
||||
required_parsers.push_back(named_arg);
|
||||
} else {
|
||||
optional_parsers.push_back(named_arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build required arg sequence in definition order
|
||||
common_peg_parser args_seq = p.eps();
|
||||
|
||||
@@ -488,7 +488,7 @@ common_peg_parser common_chat_peg_builder::standard_constructed_tools(
|
||||
}
|
||||
const auto & function = tool_def.at("function");
|
||||
std::string name = function.at("name");
|
||||
ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object();
|
||||
ordered_json params = common_chat_tool_parameters(function);
|
||||
|
||||
// Build argument parsers
|
||||
auto args = eps();
|
||||
@@ -565,7 +565,7 @@ common_peg_parser common_chat_peg_builder::python_style_tool_calls(
|
||||
}
|
||||
const auto & function = tool_def.at("function");
|
||||
std::string name = function.at("name");
|
||||
ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object();
|
||||
ordered_json params = common_chat_tool_parameters(function);
|
||||
|
||||
auto args = eps();
|
||||
if (params.contains("properties") && !params["properties"].empty()) {
|
||||
@@ -640,7 +640,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key(
|
||||
}
|
||||
const auto & function = tool_def.at("function");
|
||||
std::string name = function.at("name");
|
||||
ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object();
|
||||
ordered_json params = common_chat_tool_parameters(function);
|
||||
|
||||
// Build inner object fields
|
||||
std::vector<common_peg_parser> inner_fields;
|
||||
@@ -726,7 +726,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys(
|
||||
}
|
||||
const auto & function = tool_def.at("function");
|
||||
std::string name = function.at("name");
|
||||
ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object();
|
||||
ordered_json params = common_chat_tool_parameters(function);
|
||||
|
||||
auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() +
|
||||
atomic(literal("\"") + tool_name(literal(name)) + literal("\""));
|
||||
@@ -795,7 +795,7 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys(
|
||||
}
|
||||
const auto & function = tool_def.at("function");
|
||||
std::string name = function.at("name");
|
||||
ordered_json params = function.contains("parameters") ? function.at("parameters") : ordered_json::object();
|
||||
ordered_json params = common_chat_tool_parameters(function);
|
||||
|
||||
auto tool_name_ = name_key_parser + space() + literal(":") + space() +
|
||||
atomic(literal("\"") + tool_name(literal(name)) + literal("\""));
|
||||
|
||||
@@ -574,6 +574,16 @@ json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & t
|
||||
return result;
|
||||
}
|
||||
|
||||
json common_chat_tool_parameters(const json & function) {
|
||||
if (function.contains("parameters")) {
|
||||
const auto & params = function.at("parameters");
|
||||
if (!params.is_null() && !(params.is_object() && params.empty())) {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
return json{{"type", "object"}, {"properties", json::object()}};
|
||||
}
|
||||
|
||||
std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & tools) {
|
||||
std::vector<common_chat_tool> result;
|
||||
|
||||
|
||||
@@ -360,6 +360,9 @@ common_json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg
|
||||
|
||||
common_json common_chat_tools_to_json_oaicompat(const std::vector<common_chat_tool> & tools);
|
||||
|
||||
// The parameters schema of a function tool. A tool without parameters, or with an empty {}, takes zero arguments.
|
||||
common_json common_chat_tool_parameters(const common_json & function);
|
||||
|
||||
// get template caps, useful for reporting to server /props endpoint
|
||||
std::map<std::string, bool> common_chat_templates_get_caps(const common_chat_templates * chat_templates);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "fit.h"
|
||||
|
||||
#include "json.h"
|
||||
#include "log.h"
|
||||
|
||||
#include "../src/llama-ext.h"
|
||||
@@ -915,6 +916,9 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
|
||||
std::vector<std::array<std::string, 9>> table_data;
|
||||
table_data.reserve(devices.size());
|
||||
|
||||
// same data as the table below, for --log-jsonl consumers
|
||||
common_json rows = common_json::array();
|
||||
const std::string template_header = "%s: | %s | %s %s %s %s %s %s %s |\n";
|
||||
const std::string template_gpu = "%s: | %s | %s = %s + (%s = %s + %s + %s) + %s |\n";
|
||||
const std::string template_other = "%s: | %s | %s %s %s = %s + %s + %s %s |\n";
|
||||
@@ -989,6 +993,19 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
std::to_string(mb.context / MiB),
|
||||
std::to_string(mb.compute / MiB),
|
||||
std::to_string(unaccounted / static_cast<int64_t>(MiB))});
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "device"},
|
||||
{"name", name},
|
||||
{"description", desc},
|
||||
{"total", total / MiB},
|
||||
{"free", free / MiB},
|
||||
{"self", self / MiB},
|
||||
{"model", mb.model / MiB},
|
||||
{"context", mb.context / MiB},
|
||||
{"compute", mb.compute / MiB},
|
||||
{"unaccounted", unaccounted / static_cast<int64_t>(MiB)},
|
||||
});
|
||||
}
|
||||
|
||||
// print memory breakdown for host:
|
||||
@@ -1004,6 +1021,15 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
std::to_string(mb_host.context / MiB),
|
||||
std::to_string(mb_host.compute / MiB),
|
||||
""}); // unaccounted
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "host"},
|
||||
{"name", "Host"},
|
||||
{"self", self / MiB},
|
||||
{"model", mb_host.model / MiB},
|
||||
{"context", mb_host.context / MiB},
|
||||
{"compute", mb_host.compute / MiB},
|
||||
});
|
||||
}
|
||||
|
||||
// print memory breakdown for all remaining buffer types:
|
||||
@@ -1025,6 +1051,16 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
std::to_string(mb.context / MiB),
|
||||
std::to_string(mb.compute / MiB),
|
||||
""}); // unaccounted
|
||||
|
||||
rows.push_back({
|
||||
{"kind", "buffer_type"},
|
||||
{"name", name},
|
||||
{"self", self / MiB},
|
||||
{"model", mb.model / MiB},
|
||||
{"context", mb.context / MiB},
|
||||
{"compute", mb.compute / MiB},
|
||||
});
|
||||
|
||||
seen_buffer_types.insert(buft);
|
||||
}
|
||||
|
||||
@@ -1042,6 +1078,11 @@ void common_memory_breakdown_print(const struct llama_context * ctx) {
|
||||
__func__, td[1].c_str(), td[2].c_str(), td[3].c_str(), td[4].c_str(), td[5].c_str(),
|
||||
td[6].c_str(), td[7].c_str(), td[8].c_str());
|
||||
}
|
||||
|
||||
LOG_JSON("fit_memory_breakdown", common_json({
|
||||
{"unit", "MiB"},
|
||||
{"rows", rows},
|
||||
}));
|
||||
}
|
||||
|
||||
void common_fit_print(
|
||||
|
||||
@@ -842,6 +842,12 @@ value member_expression::execute_impl(context & ctx) {
|
||||
} else {
|
||||
property = this->property->execute(ctx);
|
||||
}
|
||||
} else if (is_stmt<integer_literal>(this->property)) {
|
||||
// syntax: obj.index
|
||||
property = mk_val<value_int>(cast_stmt<integer_literal>(this->property)->val);
|
||||
if (property->as_int() < 0) {
|
||||
throw std::runtime_error("Static member property cannot be negative");
|
||||
}
|
||||
} else {
|
||||
// syntax: obj.prop
|
||||
if (!is_stmt<identifier>(this->property)) {
|
||||
|
||||
+181
-422
@@ -1,5 +1,7 @@
|
||||
#include "json-schema-to-grammar.h"
|
||||
#include "common.h"
|
||||
#include "trie.h"
|
||||
#include "unicode.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
@@ -336,18 +338,20 @@ static size_t gbnf_escape_length(const std::string & pattern, size_t pos) {
|
||||
return 2 + n_hex;
|
||||
}
|
||||
|
||||
class common_schema_converter {
|
||||
class common_chat_schema_converter {
|
||||
private:
|
||||
friend class common_schema_info;
|
||||
friend std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options);
|
||||
std::function<json(const std::string &)> _fetch_json;
|
||||
bool _dotall;
|
||||
std::map<std::string, std::string> _rules;
|
||||
std::unordered_map<std::string, json> _refs;
|
||||
std::unordered_set<std::string> _refs_being_resolved;
|
||||
std::vector<std::string> _errors;
|
||||
std::vector<std::string> _warnings;
|
||||
|
||||
template <typename T>
|
||||
static const T & as(const common_chat_schema & node) {
|
||||
return static_cast<const T &>(node);
|
||||
}
|
||||
|
||||
std::string _add_rule(const std::string & name, const std::string & rule) {
|
||||
std::string esc_name = regex_replace(name, INVALID_RULE_CHARS_RE, "-");
|
||||
if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) {
|
||||
@@ -363,11 +367,11 @@ private:
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string _generate_union_rule(const std::string & name, const std::vector<json> & alt_schemas) {
|
||||
std::string _generate_union_rule(const std::string & name, const std::vector<common_chat_schema_ptr> & alt_schemas) {
|
||||
std::vector<std::string> rules;
|
||||
rules.reserve(alt_schemas.size());
|
||||
for (size_t i = 0; i < alt_schemas.size(); i++) {
|
||||
rules.push_back(visit(alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i)));
|
||||
rules.push_back(visit(*alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i)));
|
||||
}
|
||||
return string_join(rules, " | ");
|
||||
}
|
||||
@@ -634,85 +638,68 @@ private:
|
||||
-> ["] ( [a] ([l] ([s] ([o] char+ | [^"o] char*) | [^"s] char*) | [n] ([d] char+ | [^"d] char*) | [^"ln] char*) | [^"a] char* )? ["]
|
||||
*/
|
||||
std::string _not_strings(const std::vector<std::string> & strings) {
|
||||
|
||||
struct TrieNode {
|
||||
std::map<char, TrieNode> children;
|
||||
bool is_end_of_string;
|
||||
|
||||
TrieNode() : is_end_of_string(false) {}
|
||||
|
||||
void insert(const std::string & string) {
|
||||
auto *node = this;
|
||||
for (char c : string) {
|
||||
node = &node->children[c];
|
||||
}
|
||||
node->is_end_of_string = true;
|
||||
}
|
||||
};
|
||||
|
||||
TrieNode trie;
|
||||
for (const auto & s : strings) {
|
||||
trie.insert(s);
|
||||
}
|
||||
common_trie trie(strings);
|
||||
|
||||
std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
|
||||
std::ostringstream out;
|
||||
out << "[\"] ( ";
|
||||
std::function<void(const TrieNode &)> visit = [&](const TrieNode & node) {
|
||||
std::ostringstream rejects;
|
||||
std::function<void(size_t)> visit = [&](size_t idx) {
|
||||
const auto & node = trie.nodes[idx];
|
||||
std::string rejects;
|
||||
auto first = true;
|
||||
for (const auto & kv : node.children) {
|
||||
rejects << kv.first;
|
||||
for (const auto & [cpt, child] : node.children) {
|
||||
std::string c = common_unicode_cpt_to_utf8(cpt);
|
||||
rejects += c;
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
out << " | ";
|
||||
}
|
||||
out << "[" << kv.first << "]";
|
||||
if (!kv.second.children.empty()) {
|
||||
out << "[" << c << "]";
|
||||
if (!trie.nodes[child].children.empty()) {
|
||||
out << " (";
|
||||
visit(kv.second);
|
||||
visit(child);
|
||||
out << ")";
|
||||
} else if (kv.second.is_end_of_string) {
|
||||
} else {
|
||||
out << " " << char_rule << "+";
|
||||
}
|
||||
}
|
||||
if (!node.children.empty()) {
|
||||
if (!first) {
|
||||
out << " | ";
|
||||
}
|
||||
out << "[^\"" << rejects.str() << "] " << char_rule << "*";
|
||||
out << " | [^\"" << rejects << "] " << char_rule << "*";
|
||||
}
|
||||
};
|
||||
visit(trie);
|
||||
visit(0);
|
||||
|
||||
out << " )";
|
||||
if (!trie.is_end_of_string) {
|
||||
if (trie.nodes[0].pattern < 0) {
|
||||
out << "?";
|
||||
}
|
||||
out << " [\"]";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string _resolve_ref(const std::string & ref) {
|
||||
auto it = ref.find('#');
|
||||
std::string ref_fragment = it != std::string::npos ? ref.substr(it + 1) : ref;
|
||||
std::string _resolve_ref(const common_chat_schema_ref & schema) {
|
||||
auto it = schema.ref.find('#');
|
||||
std::string ref_fragment = it != std::string::npos ? schema.ref.substr(it + 1) : schema.ref;
|
||||
static const std::regex nonalphanumeric_regex(R"([^a-zA-Z0-9-]+)");
|
||||
std::string ref_name = "ref" + std::regex_replace(ref_fragment, nonalphanumeric_regex, "-");
|
||||
if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(ref) == _refs_being_resolved.end()) {
|
||||
_refs_being_resolved.insert(ref);
|
||||
json resolved = _refs[ref];
|
||||
ref_name = visit(resolved, ref_name);
|
||||
_refs_being_resolved.erase(ref);
|
||||
if (_rules.find(ref_name) == _rules.end() && _refs_being_resolved.find(schema.ref) == _refs_being_resolved.end()) {
|
||||
if (!schema.target) {
|
||||
_errors.push_back("Unresolved $ref " + schema.ref);
|
||||
return "";
|
||||
}
|
||||
_refs_being_resolved.insert(schema.ref);
|
||||
ref_name = visit(*schema.target, ref_name);
|
||||
_refs_being_resolved.erase(schema.ref);
|
||||
}
|
||||
return ref_name;
|
||||
}
|
||||
|
||||
std::string _build_object_rule(
|
||||
const std::vector<std::pair<std::string, json>> & properties,
|
||||
const std::vector<std::pair<std::string, const common_chat_schema *>> & properties,
|
||||
const std::unordered_set<std::string> & required,
|
||||
const std::string & name,
|
||||
const json & additional_properties)
|
||||
const common_chat_schema * additional_properties)
|
||||
{
|
||||
std::vector<std::string> required_props;
|
||||
std::vector<std::string> optional_props;
|
||||
@@ -722,7 +709,7 @@ private:
|
||||
const auto &prop_name = kv.first;
|
||||
const auto &prop_schema = kv.second;
|
||||
|
||||
std::string prop_rule_name = visit(prop_schema, name + (name.empty() ? "" : "-") + prop_name);
|
||||
std::string prop_rule_name = visit(*prop_schema, name + (name.empty() ? "" : "-") + prop_name);
|
||||
prop_kv_rule_names[prop_name] = _add_rule(
|
||||
name + (name.empty() ? "" : "-") + prop_name + "-kv",
|
||||
format_literal(json(prop_name).dump()) + " space \":\" space " + prop_rule_name
|
||||
@@ -734,10 +721,10 @@ private:
|
||||
}
|
||||
prop_names.push_back(prop_name);
|
||||
}
|
||||
if ((additional_properties.is_boolean() && additional_properties.get<bool>()) || additional_properties.is_object()) {
|
||||
if (additional_properties) {
|
||||
std::string sub_name = name + (name.empty() ? "" : "-") + "additional";
|
||||
std::string value_rule =
|
||||
additional_properties.is_object() ? visit(additional_properties, sub_name + "-value")
|
||||
additional_properties->kind() != common_chat_schema::KIND_ANY ? visit(*additional_properties, sub_name + "-value")
|
||||
: _add_primitive("value", PRIMITIVE_RULES.at("value"));
|
||||
|
||||
auto key_rule =
|
||||
@@ -825,267 +812,163 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
common_schema_converter(
|
||||
const std::function<json(const std::string &)> & fetch_json,
|
||||
bool dotall)
|
||||
: _fetch_json(fetch_json), _dotall(dotall)
|
||||
{
|
||||
explicit common_chat_schema_converter(bool dotall) : _dotall(dotall) {
|
||||
_rules["space"] = SPACE_RULE;
|
||||
}
|
||||
|
||||
void resolve_refs(json & schema, const std::string & url) {
|
||||
/*
|
||||
* Resolves all $ref fields in the given schema, fetching any remote schemas,
|
||||
* replacing each $ref with absolute reference URL and populates _refs with the
|
||||
* respective referenced (sub)schema dictionaries.
|
||||
*/
|
||||
std::function<void(json &)> visit_refs = [&](json & n) {
|
||||
if (n.is_array()) {
|
||||
for (auto & x : n) {
|
||||
visit_refs(x);
|
||||
}
|
||||
} else if (n.is_object()) {
|
||||
if (n.contains("$ref")) {
|
||||
std::string ref = n["$ref"];
|
||||
if (_refs.find(ref) == _refs.end()) {
|
||||
json target;
|
||||
if (ref.find("https://") == 0) {
|
||||
std::string base_url = ref.substr(0, ref.find('#'));
|
||||
auto it = _refs.find(base_url);
|
||||
if (it != _refs.end()) {
|
||||
target = it->second;
|
||||
} else {
|
||||
// Fetch the referenced schema and resolve its refs
|
||||
auto referenced = _fetch_json(ref);
|
||||
resolve_refs(referenced, base_url);
|
||||
_refs[base_url] = referenced;
|
||||
}
|
||||
if (ref.find('#') == std::string::npos || ref.substr(ref.find('#') + 1).empty()) {
|
||||
return;
|
||||
}
|
||||
} else if (ref.find("#/") == 0) {
|
||||
target = schema;
|
||||
n["$ref"] = url + ref;
|
||||
ref = url + ref;
|
||||
} else {
|
||||
_errors.push_back("Unsupported ref: " + ref);
|
||||
return;
|
||||
}
|
||||
std::string pointer = ref.substr(ref.find('#') + 1);
|
||||
std::vector<std::string> tokens = string_split(pointer, "/");
|
||||
for (size_t i = 1; i < tokens.size(); ++i) {
|
||||
const std::string& sel = tokens[i];
|
||||
if (target.is_object() && target.contains(sel)) {
|
||||
target = target[sel];
|
||||
} else if (target.is_array()) {
|
||||
size_t sel_index;
|
||||
try {
|
||||
sel_index = std::stoull(sel);
|
||||
} catch (const std::invalid_argument & e) {
|
||||
sel_index = target.size();
|
||||
}
|
||||
if (sel_index >= target.size()) {
|
||||
_errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
|
||||
return;
|
||||
}
|
||||
target = target[sel_index];
|
||||
} else {
|
||||
_errors.push_back("Error resolving ref " + ref + ": " + sel + " not in " + target.dump());
|
||||
return;
|
||||
}
|
||||
}
|
||||
_refs[ref] = target;
|
||||
}
|
||||
} else {
|
||||
for (const auto & kv : n.items()) {
|
||||
visit_refs(kv.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
visit_refs(schema);
|
||||
std::string add_schema(const std::string & name, const common_chat_schema & schema) {
|
||||
return visit(schema, name);
|
||||
}
|
||||
|
||||
static std::string _generate_constant_rule(const json & value) {
|
||||
return format_literal(value.dump());
|
||||
}
|
||||
|
||||
std::string visit(const json & schema, const std::string & name) {
|
||||
json schema_type = schema.contains("type") ? schema["type"] : json();
|
||||
std::string schema_format = schema.contains("format") ? schema["format"].get<std::string>() : "";
|
||||
std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name;
|
||||
std::string _visit_primitive(const std::string & rule_name, const std::string & type) {
|
||||
return _add_primitive(rule_name == "root" ? "root" : type, PRIMITIVE_RULES.at(type));
|
||||
}
|
||||
|
||||
if (schema.contains("$ref")) {
|
||||
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");
|
||||
std::vector<json> 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()) {
|
||||
std::vector<json> schema_types;
|
||||
for (const auto & t : schema_type) {
|
||||
json schema_copy(schema);
|
||||
schema_copy["type"] = t;
|
||||
schema_types.push_back(schema_copy);
|
||||
}
|
||||
return _add_rule(rule_name, _generate_union_rule(name, schema_types));
|
||||
}
|
||||
if (schema.contains("const")) {
|
||||
return _add_rule(rule_name, _generate_constant_rule(schema["const"]));
|
||||
}
|
||||
if (schema.contains("enum")) {
|
||||
std::vector<std::string> enum_values;
|
||||
for (const auto & v : schema["enum"]) {
|
||||
enum_values.push_back(_generate_constant_rule(v));
|
||||
}
|
||||
return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ")");
|
||||
}
|
||||
if ((schema_type.is_null() || schema_type == "object")
|
||||
&& (schema.contains("properties") ||
|
||||
(schema.contains("additionalProperties") && schema["additionalProperties"] != true))) {
|
||||
std::unordered_set<std::string> required;
|
||||
if (schema.contains("required") && schema["required"].is_array()) {
|
||||
for (const auto & item : schema["required"]) {
|
||||
if (item.is_string()) {
|
||||
required.insert(item.get<std::string>());
|
||||
std::string _visit_all_of(const common_chat_schema_all_of & schema, const std::string & name, const std::string & rule_name) {
|
||||
std::unordered_set<std::string> required;
|
||||
std::vector<std::pair<std::string, const common_chat_schema *>> properties;
|
||||
std::map<std::string, size_t> enum_values;
|
||||
std::function<void(const common_chat_schema &, bool)> add_component = [&](const common_chat_schema & comp, bool is_required) {
|
||||
if (comp.kind() == common_chat_schema::KIND_REF) {
|
||||
if (const auto * target = as<common_chat_schema_ref>(comp).target) {
|
||||
add_component(*target, is_required);
|
||||
}
|
||||
} else if (comp.kind() == common_chat_schema::KIND_OBJECT) {
|
||||
for (const auto & prop : as<common_chat_schema_object>(comp).properties) {
|
||||
properties.emplace_back(prop.name, prop.schema.get());
|
||||
if (is_required) {
|
||||
required.insert(prop.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::pair<std::string, json>> properties;
|
||||
if (schema.contains("properties")) {
|
||||
for (const auto & prop : schema["properties"].items()) {
|
||||
properties.emplace_back(prop.key(), prop.value());
|
||||
} else if (comp.kind() == common_chat_schema::KIND_ENUM) {
|
||||
for (const auto & v : as<common_chat_schema_enum>(comp).values) {
|
||||
enum_values[_generate_constant_rule(v)] += 1;
|
||||
}
|
||||
}
|
||||
return _add_rule(rule_name,
|
||||
_build_object_rule(
|
||||
properties, required, name,
|
||||
schema.contains("additionalProperties") ? schema["additionalProperties"] : json()));
|
||||
};
|
||||
for (const auto & child : schema.children) {
|
||||
if (child->kind() == common_chat_schema::KIND_ANY_OF) {
|
||||
for (const auto & alt : as<common_chat_schema_any_of>(*child).children) {
|
||||
add_component(*alt, false);
|
||||
}
|
||||
} else {
|
||||
add_component(*child, true);
|
||||
}
|
||||
}
|
||||
if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) {
|
||||
std::unordered_set<std::string> required;
|
||||
std::vector<std::pair<std::string, json>> properties;
|
||||
std::map<std::string, size_t> enum_values;
|
||||
const std::string& hybrid_name = name;
|
||||
std::function<void(const json &, bool)> add_component = [&](const json & comp_schema, bool is_required) {
|
||||
if (comp_schema.contains("$ref")) {
|
||||
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());
|
||||
if (is_required) {
|
||||
required.insert(prop.key());
|
||||
}
|
||||
}
|
||||
} else if (comp_schema.contains("enum")) {
|
||||
for (const auto & v : comp_schema["enum"]) {
|
||||
const auto rule = _generate_constant_rule(v);
|
||||
if (enum_values.find(rule) == enum_values.end()) {
|
||||
enum_values[rule] = 0;
|
||||
}
|
||||
enum_values[rule] += 1;
|
||||
}
|
||||
} else {
|
||||
// todo warning
|
||||
}
|
||||
};
|
||||
for (const auto & t : schema["allOf"]) {
|
||||
if (t.contains("anyOf")) {
|
||||
for (const auto & tt : t["anyOf"]) {
|
||||
add_component(tt, false);
|
||||
}
|
||||
} else {
|
||||
add_component(t, true);
|
||||
if (!enum_values.empty()) {
|
||||
std::vector<std::string> enum_intersection;
|
||||
for (const auto & p : enum_values) {
|
||||
if (p.second == schema.children.size()) {
|
||||
enum_intersection.push_back(p.first);
|
||||
}
|
||||
}
|
||||
if (!enum_values.empty()) {
|
||||
std::vector<std::string> enum_intersection;
|
||||
for (const auto & p : enum_values) {
|
||||
if (p.second == schema["allOf"].size()) {
|
||||
enum_intersection.push_back(p.first);
|
||||
}
|
||||
}
|
||||
if (!enum_intersection.empty()) {
|
||||
return _add_rule(rule_name, "(" + string_join(enum_intersection, " | ") + ")");
|
||||
}
|
||||
if (!enum_intersection.empty()) {
|
||||
return _add_rule(rule_name, "(" + string_join(enum_intersection, " | ") + ")");
|
||||
}
|
||||
return _add_rule(rule_name, _build_object_rule(properties, required, hybrid_name, json()));
|
||||
}
|
||||
if ((schema_type.is_null() || schema_type == "array") && (schema.contains("items") || schema.contains("prefixItems"))) {
|
||||
json items = schema.contains("items") ? schema["items"] : schema["prefixItems"];
|
||||
if (items.is_array()) {
|
||||
return _add_rule(rule_name, _build_object_rule(properties, required, name, nullptr));
|
||||
}
|
||||
|
||||
std::string visit(const common_chat_schema & schema, const std::string & name) {
|
||||
std::string rule_name = is_reserved_name(name) ? name + "-" : name.empty() ? "root" : name;
|
||||
std::string sub_name = name + (name.empty() ? "" : "-");
|
||||
|
||||
switch (schema.kind()) {
|
||||
case common_chat_schema::KIND_REF:
|
||||
return _add_rule(rule_name, _resolve_ref(as<common_chat_schema_ref>(schema)));
|
||||
case common_chat_schema::KIND_ANY_OF:
|
||||
return _add_rule(rule_name, _generate_union_rule(name, as<common_chat_schema_any_of>(schema).children));
|
||||
case common_chat_schema::KIND_ALL_OF:
|
||||
return _visit_all_of(as<common_chat_schema_all_of>(schema), name, rule_name);
|
||||
case common_chat_schema::KIND_CONST:
|
||||
return _add_rule(rule_name, _generate_constant_rule(as<common_chat_schema_const>(schema).value));
|
||||
case common_chat_schema::KIND_ENUM: {
|
||||
std::vector<std::string> enum_values;
|
||||
for (const auto & v : as<common_chat_schema_enum>(schema).values) {
|
||||
enum_values.push_back(_generate_constant_rule(v));
|
||||
}
|
||||
return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ")");
|
||||
}
|
||||
case common_chat_schema::KIND_OBJECT: {
|
||||
const auto & obj = as<common_chat_schema_object>(schema);
|
||||
if (obj.properties.empty() && obj.additional_properties && obj.additional_properties->kind() == common_chat_schema::KIND_ANY) {
|
||||
return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object")));
|
||||
}
|
||||
std::vector<std::pair<std::string, const common_chat_schema *>> properties;
|
||||
std::unordered_set<std::string> required;
|
||||
for (const auto & prop : obj.properties) {
|
||||
properties.emplace_back(prop.name, prop.schema.get());
|
||||
if (prop.required) {
|
||||
required.insert(prop.name);
|
||||
}
|
||||
}
|
||||
return _add_rule(rule_name, _build_object_rule(properties, required, name, obj.additional_properties.get()));
|
||||
}
|
||||
case common_chat_schema::KIND_TUPLE: {
|
||||
const auto & items = as<common_chat_schema_tuple>(schema).items;
|
||||
std::string rule = "\"[\" space ";
|
||||
for (size_t i = 0; i < items.size(); i++) {
|
||||
if (i > 0) {
|
||||
rule += " \",\" space ";
|
||||
}
|
||||
rule += visit(items[i], name + (name.empty() ? "" : "-") + "tuple-" + std::to_string(i));
|
||||
rule += visit(*items[i], sub_name + "tuple-" + std::to_string(i));
|
||||
}
|
||||
rule += " space \"]\"";
|
||||
return _add_rule(rule_name, rule);
|
||||
}
|
||||
std::string item_rule_name = visit(items, name + (name.empty() ? "" : "-") + "item");
|
||||
int min_items = schema.contains("minItems") ? schema["minItems"].get<int>() : 0;
|
||||
json max_items_json = schema.contains("maxItems") ? schema["maxItems"] : json();
|
||||
int max_items = max_items_json.is_number_integer() ? max_items_json.get<int>() : std::numeric_limits<int>::max();
|
||||
|
||||
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);
|
||||
}
|
||||
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"));
|
||||
}
|
||||
if ((schema_type.is_null() || schema_type == "string") && STRING_FORMAT_RULES.find(schema_format + "-string") != STRING_FORMAT_RULES.end()) {
|
||||
auto prim_name = schema_format + "-string";
|
||||
return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name)));
|
||||
}
|
||||
if (schema_type == "string" && (schema.contains("minLength") || schema.contains("maxLength"))) {
|
||||
std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
|
||||
int min_len = schema.contains("minLength") ? schema["minLength"].get<int>() : 0;
|
||||
int max_len = schema.contains("maxLength") ? schema["maxLength"].get<int>() : std::numeric_limits<int>::max();
|
||||
return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, min_len, max_len) + " \"\\\"\"");
|
||||
}
|
||||
if (schema_type == "integer" && (schema.contains("minimum") || schema.contains("exclusiveMinimum") || schema.contains("maximum") || schema.contains("exclusiveMaximum"))) {
|
||||
int64_t min_value = std::numeric_limits<int64_t>::min();
|
||||
int64_t max_value = std::numeric_limits<int64_t>::max();
|
||||
if (schema.contains("minimum")) {
|
||||
min_value = schema["minimum"].get<int64_t>();
|
||||
} else if (schema.contains("exclusiveMinimum")) {
|
||||
min_value = schema["exclusiveMinimum"].get<int64_t>() + 1;
|
||||
case common_chat_schema::KIND_ARRAY: {
|
||||
const auto & arr = as<common_chat_schema_array>(schema);
|
||||
if (arr.items->kind() == common_chat_schema::KIND_ANY && arr.min_items == 0 && arr.max_items < 0) {
|
||||
return _visit_primitive(rule_name, "array");
|
||||
}
|
||||
std::string item_rule_name = visit(*arr.items, sub_name + "item");
|
||||
int max_items = arr.max_items < 0 ? std::numeric_limits<int>::max() : arr.max_items;
|
||||
return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, arr.min_items, max_items, "\",\" space") + " space \"]\"");
|
||||
}
|
||||
if (schema.contains("maximum")) {
|
||||
max_value = schema["maximum"].get<int64_t>();
|
||||
} else if (schema.contains("exclusiveMaximum")) {
|
||||
max_value = schema["exclusiveMaximum"].get<int64_t>() - 1;
|
||||
case common_chat_schema::KIND_STRING: {
|
||||
const auto & str = as<common_chat_schema_string>(schema);
|
||||
if (!str.pattern.empty()) {
|
||||
return _visit_pattern(str.pattern, rule_name);
|
||||
}
|
||||
if (str.format == common_chat_schema::FORMAT_UUID) {
|
||||
return _visit_primitive(rule_name, "uuid");
|
||||
}
|
||||
if (str.format != common_chat_schema::FORMAT_NONE) {
|
||||
std::string prim_name = std::string(str.format == common_chat_schema::FORMAT_DATE ? "date" : str.format == common_chat_schema::FORMAT_TIME ? "time" : "date-time") + "-string";
|
||||
return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name)));
|
||||
}
|
||||
if (str.min_length > 0 || str.max_length >= 0) {
|
||||
std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char"));
|
||||
int max_len = str.max_length < 0 ? std::numeric_limits<int>::max() : str.max_length;
|
||||
return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, str.min_length, max_len) + " \"\\\"\"");
|
||||
}
|
||||
return _visit_primitive(rule_name, "string");
|
||||
}
|
||||
std::stringstream out;
|
||||
out << "(";
|
||||
build_min_max_int(min_value, max_value, out);
|
||||
out << ")";
|
||||
return _add_rule(rule_name, out.str());
|
||||
case common_chat_schema::KIND_INTEGER: {
|
||||
const auto & i = as<common_chat_schema_integer>(schema);
|
||||
if (i.minimum == std::numeric_limits<int64_t>::min() && i.maximum == std::numeric_limits<int64_t>::max()) {
|
||||
return _visit_primitive(rule_name, "integer");
|
||||
}
|
||||
std::stringstream out;
|
||||
out << "(";
|
||||
build_min_max_int(i.minimum, i.maximum, out);
|
||||
out << ")";
|
||||
return _add_rule(rule_name, out.str());
|
||||
}
|
||||
case common_chat_schema::KIND_NUMBER:
|
||||
return _visit_primitive(rule_name, "number");
|
||||
case common_chat_schema::KIND_BOOLEAN:
|
||||
return _visit_primitive(rule_name, "boolean");
|
||||
case common_chat_schema::KIND_NULL:
|
||||
return _visit_primitive(rule_name, "null");
|
||||
case common_chat_schema::KIND_ANY:
|
||||
return _add_rule(rule_name, _add_primitive("value", PRIMITIVE_RULES.at("value")));
|
||||
}
|
||||
if (schema.empty() || schema_type == "object") {
|
||||
return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object")));
|
||||
}
|
||||
if (schema_type.is_null() && schema.is_object()) {
|
||||
// No type constraint and no recognized structural keywords (e.g. {"description": "..."}).
|
||||
// Per JSON Schema semantics this is equivalent to {} and accepts any value.
|
||||
return _add_rule(rule_name, _add_primitive("value", PRIMITIVE_RULES.at("value")));
|
||||
}
|
||||
if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get<std::string>()) == PRIMITIVE_RULES.end()) {
|
||||
_errors.push_back("Unrecognized schema: " + schema.dump());
|
||||
return "";
|
||||
}
|
||||
// TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
|
||||
return _add_primitive(rule_name == "root" ? "root" : schema_type.get<std::string>(), PRIMITIVE_RULES.at(schema_type.get<std::string>()));
|
||||
return "";
|
||||
}
|
||||
|
||||
void check_errors() {
|
||||
@@ -1106,134 +989,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// common_schema_info implementation (pimpl)
|
||||
|
||||
common_schema_info::common_schema_info()
|
||||
: impl_(std::make_unique<common_schema_converter>(
|
||||
[](const std::string &) { return json(); },
|
||||
false)) {}
|
||||
|
||||
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(common_json & schema) {
|
||||
impl_->resolve_refs(schema, "");
|
||||
}
|
||||
|
||||
// Determines if a JSON schema can resolve to a string type through any path.
|
||||
// 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 common_json & schema) {
|
||||
std::unordered_set<std::string> visited_refs;
|
||||
|
||||
std::function<bool(const json &)> check = [&](const json & s) -> bool {
|
||||
if (!s.is_object()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle $ref
|
||||
if (s.contains("$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;
|
||||
}
|
||||
visited_refs.insert(ref);
|
||||
auto it = impl_->_refs.find(ref);
|
||||
if (it != impl_->_refs.end()) {
|
||||
return check(it->second);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check type field
|
||||
if (s.contains("type")) {
|
||||
const json & schema_type = s["type"];
|
||||
if (schema_type.is_string()) {
|
||||
if (schema_type == "string") {
|
||||
return true;
|
||||
}
|
||||
} else if (schema_type.is_array()) {
|
||||
// Type can be an array like ["string", "null"]
|
||||
for (const auto & t : schema_type) {
|
||||
if (t == "string") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check oneOf/anyOf - if any alternative can be a string
|
||||
if (s.contains("oneOf")) {
|
||||
for (const auto & alt : s["oneOf"]) {
|
||||
if (check(alt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.contains("anyOf")) {
|
||||
for (const auto & alt : s["anyOf"]) {
|
||||
if (check(alt)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check allOf - all components must be compatible with string type
|
||||
if (s.contains("allOf")) {
|
||||
bool all_string = true;
|
||||
for (const auto & component : s["allOf"]) {
|
||||
if (!check(component)) {
|
||||
all_string = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_string) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check const - if the constant value is a string
|
||||
if (s.contains("const")) {
|
||||
if (s["const"].is_string()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check enum - if any enum value is a string
|
||||
if (s.contains("enum")) {
|
||||
for (const auto & val : s["enum"]) {
|
||||
if (val.is_string()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String-specific keywords imply string type
|
||||
if (s.contains("pattern") || s.contains("minLength") || s.contains("maxLength")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check format - many formats imply string
|
||||
if (s.contains("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" ||
|
||||
fmt.find("uuid") == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return check(schema);
|
||||
}
|
||||
|
||||
std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf) {
|
||||
#ifdef LLAMA_USE_LLGUIDANCE
|
||||
if (!force_gbnf) {
|
||||
@@ -1242,25 +997,29 @@ std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf)
|
||||
#else
|
||||
(void)force_gbnf;
|
||||
#endif // LLAMA_USE_LLGUIDANCE
|
||||
return build_grammar([&](const common_grammar_builder & callbacks) {
|
||||
auto copy = schema;
|
||||
callbacks.resolve_refs(copy);
|
||||
callbacks.add_schema("", copy);
|
||||
});
|
||||
try {
|
||||
return json_schema_to_grammar(common_chat_schema_from_json(schema));
|
||||
} catch (const std::runtime_error & e) {
|
||||
throw std::invalid_argument(std::string("JSON schema conversion failed:\n") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string json_schema_to_grammar(const common_chat_schema_document & schema) {
|
||||
common_chat_schema_converter converter(false);
|
||||
converter.visit(*schema.root, "");
|
||||
converter.check_errors();
|
||||
return converter.format_grammar();
|
||||
}
|
||||
|
||||
std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options) {
|
||||
common_schema_converter converter([&](const std::string &) { return json(); }, options.dotall);
|
||||
common_chat_schema_converter converter(options.dotall);
|
||||
common_grammar_builder builder {
|
||||
/* .add_rule = */ [&](const std::string & name, const std::string & rule) {
|
||||
return converter._add_rule(name, rule);
|
||||
},
|
||||
/* .add_schema = */ [&](const std::string & name, const common_json & schema) {
|
||||
return converter.visit(schema, name == "root" ? "" : name);
|
||||
/* .add_schema = */ [&](const std::string & name, const common_chat_schema & schema) {
|
||||
return converter.add_schema(name == "root" ? "" : name, schema);
|
||||
},
|
||||
/* .resolve_refs = */ [&](common_json & schema) {
|
||||
converter.resolve_refs(schema, "");
|
||||
}
|
||||
};
|
||||
cb(builder);
|
||||
converter.check_errors();
|
||||
|
||||
@@ -1,37 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "json-schema.h"
|
||||
#include "json.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
std::string json_schema_to_grammar(const common_json & schema,
|
||||
bool force_gbnf = false);
|
||||
|
||||
class common_schema_converter;
|
||||
|
||||
// Probes a JSON schema to extract information about its structure and type constraints.
|
||||
class common_schema_info {
|
||||
std::unique_ptr<common_schema_converter> impl_;
|
||||
|
||||
public:
|
||||
common_schema_info();
|
||||
~common_schema_info();
|
||||
|
||||
common_schema_info(const common_schema_info &) = delete;
|
||||
common_schema_info & operator=(const common_schema_info &) = delete;
|
||||
common_schema_info(common_schema_info &&) noexcept;
|
||||
common_schema_info & operator=(common_schema_info &&) noexcept;
|
||||
|
||||
void resolve_refs(common_json & schema);
|
||||
bool resolves_to_string(const common_json & schema);
|
||||
};
|
||||
std::string json_schema_to_grammar(const common_json & schema, bool force_gbnf = false);
|
||||
std::string json_schema_to_grammar(const common_chat_schema_document & schema);
|
||||
|
||||
struct common_grammar_builder {
|
||||
std::function<std::string(const std::string &, const std::string &)> add_rule;
|
||||
std::function<std::string(const std::string &, const common_json &)> add_schema;
|
||||
std::function<void(common_json &)> resolve_refs;
|
||||
std::function<std::string(const std::string &, const std::string &)> add_rule;
|
||||
std::function<std::string(const std::string &, const common_chat_schema &)> add_schema;
|
||||
};
|
||||
|
||||
struct common_grammar_options {
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
#include "json-schema.h"
|
||||
#include "common.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class common_chat_schema_builder {
|
||||
const common_json & root_;
|
||||
common_chat_schema_document & doc_;
|
||||
|
||||
// the targets built here, moved into doc_ once the whole schema is built
|
||||
std::map<std::string, common_chat_schema_ptr> refs_;
|
||||
|
||||
// ref nodes get their target once every $ref is built, a cycle would otherwise need it too early
|
||||
std::vector<common_chat_schema_ref *> pending_;
|
||||
|
||||
[[noreturn]] static void fail(const std::string & path, const std::string & msg) {
|
||||
throw std::runtime_error("JSON schema error at " + path + ": " + msg);
|
||||
}
|
||||
|
||||
static int get_count(const common_json & schema, const std::string & key, const std::string & path, int def) {
|
||||
if (!schema.contains(key)) {
|
||||
return def;
|
||||
}
|
||||
const common_json & value = schema.at(key);
|
||||
if (!value.is_number_integer() || value.get<int>() < 0) {
|
||||
fail(path, key + " must be a non-negative integer");
|
||||
}
|
||||
return value.get<int>();
|
||||
}
|
||||
|
||||
// a fractional bound is rounded inwards, towards the integers it still admits
|
||||
static int64_t get_bound(const common_json & schema, const std::string & key, const std::string & path, bool round_up) {
|
||||
const common_json & value = schema.at(key);
|
||||
if (value.is_number_integer()) {
|
||||
return value.get<int64_t>();
|
||||
}
|
||||
if (!value.is_number()) {
|
||||
fail(path, key + " must be a number");
|
||||
}
|
||||
double d = value.get<double>();
|
||||
return (int64_t) (round_up ? std::ceil(d) : std::floor(d));
|
||||
}
|
||||
|
||||
static common_chat_schema::string_format get_format(const common_json & schema, const std::string & path) {
|
||||
if (!schema.contains("format")) {
|
||||
return common_chat_schema::FORMAT_NONE;
|
||||
}
|
||||
const common_json & value = schema.at("format");
|
||||
if (!value.is_string()) {
|
||||
fail(path, "format must be a string");
|
||||
}
|
||||
std::string format = value.get<std::string>();
|
||||
if (format == "date") {
|
||||
return common_chat_schema::FORMAT_DATE;
|
||||
}
|
||||
if (format == "time") {
|
||||
return common_chat_schema::FORMAT_TIME;
|
||||
}
|
||||
if (format == "date-time") {
|
||||
return common_chat_schema::FORMAT_DATE_TIME;
|
||||
}
|
||||
if (format == "uuid" || (format.size() == 5 && format.compare(0, 4, "uuid") == 0 && format[4] >= '1' && format[4] <= '5')) {
|
||||
return common_chat_schema::FORMAT_UUID;
|
||||
}
|
||||
return common_chat_schema::FORMAT_NONE;
|
||||
}
|
||||
|
||||
const common_json & resolve_ref(const std::string & ref, const std::string & path) {
|
||||
const common_json * target = &root_;
|
||||
auto tokens = string_split(ref.substr(1), "/");
|
||||
for (size_t i = 1; i < tokens.size(); i++) {
|
||||
const std::string & sel = tokens[i];
|
||||
if (target->is_object() && target->contains(sel)) {
|
||||
target = &target->at(sel);
|
||||
} else if (target->is_array()) {
|
||||
size_t idx;
|
||||
try {
|
||||
idx = std::stoull(sel);
|
||||
} catch (const std::logic_error &) {
|
||||
idx = target->size();
|
||||
}
|
||||
if (idx >= target->size()) {
|
||||
fail(path, "cannot resolve $ref " + ref + ", " + sel + " is out of range");
|
||||
}
|
||||
target = &target->at(idx);
|
||||
} else {
|
||||
fail(path, "cannot resolve $ref " + ref + ", " + sel + " not found");
|
||||
}
|
||||
}
|
||||
return *target;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_ref(const common_json & value, const std::string & path) {
|
||||
if (!value.is_string()) {
|
||||
fail(path, "$ref must be a string");
|
||||
}
|
||||
std::string ref = value.get<std::string>();
|
||||
if (ref.compare(0, 2, "#/") != 0) {
|
||||
fail(path, "unsupported $ref " + ref + ", only references into the same document are supported");
|
||||
}
|
||||
if (refs_.find(ref) == refs_.end()) {
|
||||
// reserve the key first, so that a cycle back to this $ref stops here
|
||||
refs_[ref] = nullptr;
|
||||
refs_[ref] = build_node(resolve_ref(ref, path), ref);
|
||||
}
|
||||
auto node = std::make_unique<common_chat_schema_ref>(ref);
|
||||
pending_.push_back(node.get());
|
||||
return node;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
common_chat_schema_ptr build_alternatives(const common_json & alts, const std::string & path) {
|
||||
if (!alts.is_array()) {
|
||||
fail(path, "must be an array of schemas");
|
||||
}
|
||||
if (alts.empty()) {
|
||||
fail(path, "must not be empty");
|
||||
}
|
||||
auto node = std::make_unique<T>();
|
||||
size_t i = 0;
|
||||
for (const auto & alt : alts) {
|
||||
node->children.push_back(build_node(alt, path + "/" + std::to_string(i++)));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_object(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_chat_schema_object>();
|
||||
|
||||
std::unordered_set<std::string> required;
|
||||
if (schema.contains("required") && schema.at("required").is_array()) {
|
||||
for (const auto & name : schema.at("required")) {
|
||||
if (name.is_string()) {
|
||||
required.insert(name.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.contains("properties")) {
|
||||
const common_json & properties = schema.at("properties");
|
||||
if (!properties.is_object()) {
|
||||
fail(path, "properties must be an object");
|
||||
}
|
||||
for (const auto & [name, prop] : properties.items()) {
|
||||
node->properties.push_back({name, build_node(prop, path + "/properties/" + name), required.count(name) > 0});
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.contains("additionalProperties")) {
|
||||
const common_json & additional = schema.at("additionalProperties");
|
||||
if (additional.is_boolean()) {
|
||||
if (additional.get<bool>()) {
|
||||
node->additional_properties = std::make_unique<common_chat_schema_any>();
|
||||
}
|
||||
} else if (additional.is_object()) {
|
||||
node->additional_properties = build_node(additional, path + "/additionalProperties");
|
||||
} else {
|
||||
fail(path, "additionalProperties must be a boolean or a schema");
|
||||
}
|
||||
} else if (!schema.contains("properties")) {
|
||||
// {"type": "object"} on its own accepts any object
|
||||
node->additional_properties = std::make_unique<common_chat_schema_any>();
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_array(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_chat_schema_array>();
|
||||
if (schema.contains("items") || schema.contains("prefixItems")) {
|
||||
// "items" wins when both are present; as in the converter, a schema instead of an array is the item schema
|
||||
const std::string key = schema.contains("items") ? "items" : "prefixItems";
|
||||
const common_json & items = schema.at(key);
|
||||
if (items.is_array()) {
|
||||
auto tuple = std::make_unique<common_chat_schema_tuple>();
|
||||
size_t i = 0;
|
||||
for (const auto & item : items) {
|
||||
tuple->items.push_back(build_node(item, path + "/" + key + "/" + std::to_string(i++)));
|
||||
}
|
||||
return tuple;
|
||||
}
|
||||
node->items = build_node(items, path + "/" + key);
|
||||
} else {
|
||||
node->items = std::make_unique<common_chat_schema_any>();
|
||||
}
|
||||
node->min_items = get_count(schema, "minItems", path, 0);
|
||||
node->max_items = get_count(schema, "maxItems", path, -1);
|
||||
return node;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_string(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_chat_schema_string>();
|
||||
if (schema.contains("pattern")) {
|
||||
const common_json & pattern = schema.at("pattern");
|
||||
if (!pattern.is_string()) {
|
||||
fail(path, "pattern must be a string");
|
||||
}
|
||||
node->pattern = pattern.get<std::string>();
|
||||
}
|
||||
node->format = get_format(schema, path);
|
||||
node->min_length = get_count(schema, "minLength", path, 0);
|
||||
node->max_length = get_count(schema, "maxLength", path, -1);
|
||||
return node;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_integer(const common_json & schema, const std::string & path) {
|
||||
auto node = std::make_unique<common_chat_schema_integer>();
|
||||
if (schema.contains("minimum")) {
|
||||
node->minimum = get_bound(schema, "minimum", path, /* round_up */ true);
|
||||
} else if (schema.contains("exclusiveMinimum")) {
|
||||
node->minimum = get_bound(schema, "exclusiveMinimum", path, /* round_up */ false) + 1;
|
||||
}
|
||||
if (schema.contains("maximum")) {
|
||||
node->maximum = get_bound(schema, "maximum", path, /* round_up */ false);
|
||||
} else if (schema.contains("exclusiveMaximum")) {
|
||||
node->maximum = get_bound(schema, "exclusiveMaximum", path, /* round_up */ true) - 1;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
common_chat_schema_ptr build_node(const common_json & schema, const std::string & path) {
|
||||
if (!schema.is_object()) {
|
||||
fail(path, "schema must be an object");
|
||||
}
|
||||
if (schema.contains("$ref")) {
|
||||
return build_ref(schema.at("$ref"), path);
|
||||
}
|
||||
if (schema.contains("oneOf") || schema.contains("anyOf")) {
|
||||
const std::string key = schema.contains("oneOf") ? "oneOf" : "anyOf";
|
||||
return build_alternatives<common_chat_schema_any_of>(schema.at(key), path + "/" + key);
|
||||
}
|
||||
|
||||
common_json type;
|
||||
if (schema.contains("type")) {
|
||||
type = schema.at("type");
|
||||
}
|
||||
if (type.is_array()) {
|
||||
// {"type": ["a", "b"], ...} is {"anyOf": [{"type": "a", ...}, {"type": "b", ...}]}
|
||||
if (type.empty()) {
|
||||
fail(path, "type must not be empty");
|
||||
}
|
||||
auto node = std::make_unique<common_chat_schema_any_of>();
|
||||
size_t i = 0;
|
||||
for (const auto & t : type) {
|
||||
common_json alt = schema;
|
||||
alt["type"] = t;
|
||||
node->children.push_back(build_node(alt, path + "/type/" + std::to_string(i++)));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
if (schema.contains("const")) {
|
||||
return std::make_unique<common_chat_schema_const>(schema.at("const"));
|
||||
}
|
||||
if (schema.contains("enum")) {
|
||||
const common_json & values = schema.at("enum");
|
||||
if (!values.is_array() || values.empty()) {
|
||||
fail(path, "enum must be a non-empty array");
|
||||
}
|
||||
auto node = std::make_unique<common_chat_schema_enum>();
|
||||
for (const auto & value : values) {
|
||||
node->values.push_back(value);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
if (!type.is_null() && !type.is_string()) {
|
||||
fail(path, "type must be a string or an array of strings");
|
||||
}
|
||||
|
||||
const std::string type_name = type.is_string() ? type.get<std::string>() : "";
|
||||
const bool has_properties = schema.contains("properties") ||
|
||||
(schema.contains("additionalProperties") && schema.at("additionalProperties") != true);
|
||||
|
||||
if (type_name.empty()) {
|
||||
// without a type the structural keywords decide, in the same order as the converter
|
||||
if (has_properties) {
|
||||
return build_object(schema, path);
|
||||
}
|
||||
if (schema.contains("allOf")) {
|
||||
return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
if (schema.contains("items") || schema.contains("prefixItems")) {
|
||||
return build_array(schema, path);
|
||||
}
|
||||
if (schema.contains("pattern") || schema.contains("minLength") || schema.contains("maxLength") || get_format(schema, path) != common_chat_schema::FORMAT_NONE) {
|
||||
return build_string(schema, path);
|
||||
}
|
||||
return std::make_unique<common_chat_schema_any>();
|
||||
}
|
||||
if (type_name == "object") {
|
||||
if (!has_properties && schema.contains("allOf")) {
|
||||
return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
return build_object(schema, path);
|
||||
}
|
||||
if (type_name == "string") {
|
||||
if (schema.contains("allOf")) {
|
||||
return build_alternatives<common_chat_schema_all_of>(schema.at("allOf"), path + "/allOf");
|
||||
}
|
||||
return build_string(schema, path);
|
||||
}
|
||||
if (type_name == "array") {
|
||||
return build_array(schema, path);
|
||||
}
|
||||
if (type_name == "integer") {
|
||||
return build_integer(schema, path);
|
||||
}
|
||||
if (type_name == "number") {
|
||||
return std::make_unique<common_chat_schema_number>();
|
||||
}
|
||||
if (type_name == "boolean") {
|
||||
return std::make_unique<common_chat_schema_boolean>();
|
||||
}
|
||||
if (type_name == "null") {
|
||||
return std::make_unique<common_chat_schema_null>();
|
||||
}
|
||||
fail(path, "unrecognized type " + type_name);
|
||||
}
|
||||
|
||||
public:
|
||||
common_chat_schema_builder(const common_json & root, common_chat_schema_document & doc) : root_(root), doc_(doc) {}
|
||||
|
||||
common_chat_schema_ptr build() {
|
||||
auto node = build_node(root_, "#");
|
||||
for (auto & entry : refs_) {
|
||||
doc_.refs[entry.first] = std::move(entry.second);
|
||||
}
|
||||
for (auto * ref : pending_) {
|
||||
ref->target = doc_.refs.at(ref->ref).get();
|
||||
}
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
common_chat_schema_document common_chat_schema_from_json(const common_json & schema) {
|
||||
common_chat_schema_document doc;
|
||||
doc.root = common_chat_schema_builder(schema, doc).build();
|
||||
return doc;
|
||||
}
|
||||
|
||||
static common_chat_schema::value_type json_type(const common_json & value) {
|
||||
if (value.is_null()) {
|
||||
return common_chat_schema::TYPE_NULL;
|
||||
}
|
||||
if (value.is_boolean()) {
|
||||
return common_chat_schema::TYPE_BOOLEAN;
|
||||
}
|
||||
if (value.is_number_integer()) {
|
||||
return common_chat_schema::TYPE_INTEGER;
|
||||
}
|
||||
if (value.is_number()) {
|
||||
return common_chat_schema::TYPE_NUMBER;
|
||||
}
|
||||
if (value.is_string()) {
|
||||
return common_chat_schema::TYPE_STRING;
|
||||
}
|
||||
if (value.is_array()) {
|
||||
return common_chat_schema::TYPE_ARRAY;
|
||||
}
|
||||
return common_chat_schema::TYPE_OBJECT;
|
||||
}
|
||||
|
||||
static common_chat_schema::type_set value_types_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {
|
||||
switch (s.kind()) {
|
||||
case common_chat_schema::KIND_ANY:
|
||||
return common_chat_schema::type_set::all();
|
||||
case common_chat_schema::KIND_NULL:
|
||||
return { common_chat_schema::TYPE_NULL };
|
||||
case common_chat_schema::KIND_BOOLEAN:
|
||||
return { common_chat_schema::TYPE_BOOLEAN };
|
||||
case common_chat_schema::KIND_NUMBER:
|
||||
return { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER };
|
||||
case common_chat_schema::KIND_INTEGER:
|
||||
return { common_chat_schema::TYPE_INTEGER };
|
||||
case common_chat_schema::KIND_STRING:
|
||||
return { common_chat_schema::TYPE_STRING };
|
||||
case common_chat_schema::KIND_ARRAY:
|
||||
case common_chat_schema::KIND_TUPLE:
|
||||
return { common_chat_schema::TYPE_ARRAY };
|
||||
case common_chat_schema::KIND_OBJECT:
|
||||
return { common_chat_schema::TYPE_OBJECT };
|
||||
case common_chat_schema::KIND_CONST:
|
||||
return { json_type(static_cast<const common_chat_schema_const &>(s).value) };
|
||||
case common_chat_schema::KIND_ENUM: {
|
||||
common_chat_schema::type_set types;
|
||||
for (const auto & value : static_cast<const common_chat_schema_enum &>(s).values) {
|
||||
types.add(json_type(value));
|
||||
}
|
||||
return types;
|
||||
}
|
||||
case common_chat_schema::KIND_REF: {
|
||||
const auto * target = static_cast<const common_chat_schema_ref &>(s).target;
|
||||
if (!target || !visited.insert(target).second) {
|
||||
// a cycle contributes no type, to be safe
|
||||
return {};
|
||||
}
|
||||
auto types = value_types_impl(*target, visited);
|
||||
visited.erase(target);
|
||||
return types;
|
||||
}
|
||||
case common_chat_schema::KIND_ANY_OF: {
|
||||
common_chat_schema::type_set types;
|
||||
for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {
|
||||
types |= value_types_impl(*child, visited);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
case common_chat_schema::KIND_ALL_OF: {
|
||||
auto types = common_chat_schema::type_set::all();
|
||||
for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) {
|
||||
types &= value_types_impl(*child, visited);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
common_chat_schema::type_set common_chat_schema::value_types() const {
|
||||
std::unordered_set<const common_chat_schema *> visited;
|
||||
return value_types_impl(*this, visited);
|
||||
}
|
||||
|
||||
static bool may_be_string_impl(const common_chat_schema & s, std::unordered_set<const common_chat_schema *> & visited) {
|
||||
switch (s.kind()) {
|
||||
case common_chat_schema::KIND_STRING:
|
||||
return true;
|
||||
case common_chat_schema::KIND_CONST:
|
||||
return static_cast<const common_chat_schema_const &>(s).value.is_string();
|
||||
case common_chat_schema::KIND_ENUM:
|
||||
for (const auto & v : static_cast<const common_chat_schema_enum &>(s).values) {
|
||||
if (v.is_string()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case common_chat_schema::KIND_REF: {
|
||||
// a cycle is taken as not a string, to be safe
|
||||
const auto * target = static_cast<const common_chat_schema_ref &>(s).target;
|
||||
if (!target || !visited.insert(target).second) {
|
||||
return false;
|
||||
}
|
||||
bool result = may_be_string_impl(*target, visited);
|
||||
visited.erase(target);
|
||||
return result;
|
||||
}
|
||||
case common_chat_schema::KIND_ANY_OF:
|
||||
for (const auto & child : static_cast<const common_chat_schema_any_of &>(s).children) {
|
||||
if (may_be_string_impl(*child, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case common_chat_schema::KIND_ALL_OF: {
|
||||
// every child must allow a string, an any child constrains nothing
|
||||
bool any_string = false;
|
||||
for (const auto & child : static_cast<const common_chat_schema_all_of &>(s).children) {
|
||||
if (child->kind() == common_chat_schema::KIND_ANY) {
|
||||
continue;
|
||||
}
|
||||
if (!may_be_string_impl(*child, visited)) {
|
||||
return false;
|
||||
}
|
||||
any_string = true;
|
||||
}
|
||||
return any_string;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool common_chat_schema::may_be_string() const {
|
||||
std::unordered_set<const common_chat_schema *> visited;
|
||||
return may_be_string_impl(*this, visited);
|
||||
}
|
||||
|
||||
const char * common_chat_schema::kind_name(node_kind kind) {
|
||||
switch (kind) {
|
||||
case KIND_ANY: return "any";
|
||||
case KIND_REF: return "ref";
|
||||
case KIND_ANY_OF: return "anyOf";
|
||||
case KIND_ALL_OF: return "allOf";
|
||||
case KIND_CONST: return "const";
|
||||
case KIND_ENUM: return "enum";
|
||||
case KIND_NULL: return "null";
|
||||
case KIND_BOOLEAN: return "boolean";
|
||||
case KIND_NUMBER: return "number";
|
||||
case KIND_INTEGER: return "integer";
|
||||
case KIND_STRING: return "string";
|
||||
case KIND_ARRAY: return "array";
|
||||
case KIND_TUPLE: return "tuple";
|
||||
case KIND_OBJECT: return "object";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char * common_chat_schema::type_name(value_type type) {
|
||||
switch (type) {
|
||||
case TYPE_NULL: return "null";
|
||||
case TYPE_BOOLEAN: return "boolean";
|
||||
case TYPE_NUMBER: return "number";
|
||||
case TYPE_INTEGER: return "integer";
|
||||
case TYPE_STRING: return "string";
|
||||
case TYPE_ARRAY: return "array";
|
||||
case TYPE_OBJECT: return "object";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// JSON schema, covering the subset that json_schema_to_grammar() can convert.
|
||||
|
||||
struct common_chat_schema {
|
||||
enum node_kind {
|
||||
KIND_ANY,
|
||||
KIND_REF,
|
||||
KIND_ANY_OF,
|
||||
KIND_ALL_OF,
|
||||
KIND_CONST,
|
||||
KIND_ENUM,
|
||||
KIND_NULL,
|
||||
KIND_BOOLEAN,
|
||||
KIND_NUMBER,
|
||||
KIND_INTEGER,
|
||||
KIND_STRING,
|
||||
KIND_ARRAY,
|
||||
KIND_TUPLE,
|
||||
KIND_OBJECT,
|
||||
};
|
||||
|
||||
enum value_type {
|
||||
TYPE_NULL,
|
||||
TYPE_BOOLEAN,
|
||||
TYPE_NUMBER,
|
||||
TYPE_INTEGER,
|
||||
TYPE_STRING,
|
||||
TYPE_ARRAY,
|
||||
TYPE_OBJECT,
|
||||
};
|
||||
|
||||
enum string_format {
|
||||
FORMAT_NONE,
|
||||
FORMAT_UUID, // uuid, uuid1 .. uuid5
|
||||
FORMAT_DATE,
|
||||
FORMAT_TIME,
|
||||
FORMAT_DATE_TIME,
|
||||
};
|
||||
|
||||
class type_set {
|
||||
uint32_t mask_ = 0;
|
||||
|
||||
public:
|
||||
type_set() = default;
|
||||
type_set(std::initializer_list<value_type> types) {
|
||||
for (auto type : types) {
|
||||
add(type);
|
||||
}
|
||||
}
|
||||
|
||||
static type_set all() {
|
||||
return { TYPE_NULL, TYPE_BOOLEAN, TYPE_NUMBER, TYPE_INTEGER, TYPE_STRING, TYPE_ARRAY, TYPE_OBJECT };
|
||||
}
|
||||
|
||||
void add(value_type type) { mask_ |= 1u << type; }
|
||||
|
||||
bool has(value_type type) const { return (mask_ & (1u << type)) != 0; }
|
||||
bool is_only(value_type type) const { return mask_ == (1u << type); }
|
||||
bool empty() const { return mask_ == 0; }
|
||||
|
||||
type_set & operator|=(const type_set & other) { mask_ |= other.mask_; return *this; }
|
||||
type_set & operator&=(const type_set & other) { mask_ &= other.mask_; return *this; }
|
||||
|
||||
bool operator==(const type_set & other) const { return mask_ == other.mask_; }
|
||||
bool operator!=(const type_set & other) const { return mask_ != other.mask_; }
|
||||
};
|
||||
|
||||
virtual ~common_chat_schema() = default;
|
||||
virtual node_kind kind() const = 0;
|
||||
|
||||
type_set value_types() const;
|
||||
|
||||
// Whether a value matching the schema may be a string, through any branch of it.
|
||||
bool may_be_string() const;
|
||||
|
||||
static const char * kind_name(node_kind kind);
|
||||
static const char * type_name(value_type type);
|
||||
};
|
||||
|
||||
using common_chat_schema_ptr = std::unique_ptr<common_chat_schema>;
|
||||
|
||||
struct common_chat_schema_any : common_chat_schema {
|
||||
node_kind kind() const override { return KIND_ANY; }
|
||||
};
|
||||
|
||||
// {"$ref": "#/..."}, only references into the same document are supported
|
||||
struct common_chat_schema_ref : common_chat_schema {
|
||||
std::string ref;
|
||||
const common_chat_schema * target = nullptr; // owned by common_chat_schema_document::refs
|
||||
|
||||
explicit common_chat_schema_ref(std::string ref) : ref(std::move(ref)) {}
|
||||
|
||||
node_kind kind() const override { return KIND_REF; }
|
||||
};
|
||||
|
||||
// oneOf / anyOf, or a "type" array expanded to one alternative per type
|
||||
struct common_chat_schema_any_of : common_chat_schema {
|
||||
std::vector<common_chat_schema_ptr> children;
|
||||
|
||||
node_kind kind() const override { return KIND_ANY_OF; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_all_of : common_chat_schema {
|
||||
std::vector<common_chat_schema_ptr> children;
|
||||
|
||||
node_kind kind() const override { return KIND_ALL_OF; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_const : common_chat_schema {
|
||||
common_json value;
|
||||
|
||||
explicit common_chat_schema_const(common_json value) : value(std::move(value)) {}
|
||||
|
||||
node_kind kind() const override { return KIND_CONST; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_enum : common_chat_schema {
|
||||
std::vector<common_json> values;
|
||||
|
||||
node_kind kind() const override { return KIND_ENUM; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_null : common_chat_schema {
|
||||
node_kind kind() const override { return KIND_NULL; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_boolean : common_chat_schema {
|
||||
node_kind kind() const override { return KIND_BOOLEAN; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_number : common_chat_schema {
|
||||
node_kind kind() const override { return KIND_NUMBER; }
|
||||
};
|
||||
|
||||
// bounds are inclusive, exclusiveMinimum / exclusiveMaximum are folded in
|
||||
struct common_chat_schema_integer : common_chat_schema {
|
||||
int64_t minimum = INT64_MIN; // INT64_MIN for unbounded
|
||||
int64_t maximum = INT64_MAX; // INT64_MAX for unbounded
|
||||
|
||||
node_kind kind() const override { return KIND_INTEGER; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_string : common_chat_schema {
|
||||
std::string pattern; // empty when absent
|
||||
string_format format = FORMAT_NONE;
|
||||
int min_length = 0;
|
||||
int max_length = -1; // -1 for unbounded
|
||||
|
||||
node_kind kind() const override { return KIND_STRING; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_array : common_chat_schema {
|
||||
common_chat_schema_ptr items; // a common_chat_schema_any when "items" is absent
|
||||
int min_items = 0;
|
||||
int max_items = -1; // -1 for unbounded
|
||||
|
||||
node_kind kind() const override { return KIND_ARRAY; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_tuple : common_chat_schema {
|
||||
std::vector<common_chat_schema_ptr> items;
|
||||
|
||||
node_kind kind() const override { return KIND_TUPLE; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_property {
|
||||
std::string name;
|
||||
common_chat_schema_ptr schema;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
struct common_chat_schema_object : common_chat_schema {
|
||||
std::vector<common_chat_schema_property> properties; // in schema order
|
||||
common_chat_schema_ptr additional_properties; // null when not allowed
|
||||
|
||||
node_kind kind() const override { return KIND_OBJECT; }
|
||||
};
|
||||
|
||||
struct common_chat_schema_document {
|
||||
common_chat_schema_ptr root;
|
||||
std::map<std::string, common_chat_schema_ptr> refs;
|
||||
};
|
||||
|
||||
// A document shared by the PEG parsers built from its nodes, which it keeps alive
|
||||
using common_chat_schema_document_ptr = std::shared_ptr<const common_chat_schema_document>;
|
||||
|
||||
// Throws std::runtime_error when the schema falls outside the supported subset.
|
||||
common_chat_schema_document common_chat_schema_from_json(const common_json & schema);
|
||||
+63
-13
@@ -37,6 +37,16 @@ void common_log_set_verbosity_thold(int verbosity) {
|
||||
common_log_verbosity_thold = verbosity;
|
||||
}
|
||||
|
||||
static bool common_log_jsonl = false;
|
||||
|
||||
bool common_log_get_jsonl(void) {
|
||||
return common_log_jsonl;
|
||||
}
|
||||
|
||||
void common_log_set_jsonl(bool jsonl) {
|
||||
common_log_jsonl = jsonl;
|
||||
}
|
||||
|
||||
static int64_t t_us() {
|
||||
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
@@ -87,6 +97,7 @@ struct common_log_entry {
|
||||
bool is_end { false }; // signals the worker thread to stop
|
||||
bool prefix { false };
|
||||
bool jsonl { false };
|
||||
bool is_json { false }; // msg already holds a serialized JSON object
|
||||
|
||||
common_log_entry(size_t size = 256) : msg(size) { }
|
||||
|
||||
@@ -107,6 +118,12 @@ struct common_log_entry {
|
||||
}
|
||||
|
||||
if (jsonl) {
|
||||
if (is_json) {
|
||||
fprintf(fcur, "%s\n", msg.data());
|
||||
fflush(fcur);
|
||||
return;
|
||||
}
|
||||
|
||||
common_json obj = {
|
||||
{"type", "log"},
|
||||
{"time", timestamp},
|
||||
@@ -156,7 +173,6 @@ struct common_log {
|
||||
file = nullptr;
|
||||
prefix = false;
|
||||
timestamps = false;
|
||||
jsonl = false;
|
||||
running = false;
|
||||
t_start = t_us();
|
||||
|
||||
@@ -184,7 +200,6 @@ private:
|
||||
|
||||
bool prefix;
|
||||
bool timestamps;
|
||||
bool jsonl;
|
||||
bool running;
|
||||
|
||||
int64_t t_start;
|
||||
@@ -273,7 +288,8 @@ public:
|
||||
entry.is_end = false;
|
||||
entry.level = level;
|
||||
entry.prefix = prefix;
|
||||
entry.jsonl = jsonl;
|
||||
entry.jsonl = common_log_jsonl;
|
||||
entry.is_json = false;
|
||||
entry.timestamp = 0;
|
||||
if (timestamps) {
|
||||
entry.timestamp = t_us() - t_start;
|
||||
@@ -283,6 +299,42 @@ public:
|
||||
cv_new.notify_one();
|
||||
}
|
||||
|
||||
void add_json(const char * type, const common_json & obj) {
|
||||
const common_json full = {
|
||||
{"type", type},
|
||||
{"data", obj},
|
||||
};
|
||||
|
||||
const std::string text = full.dump_safe();
|
||||
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
|
||||
// block if the queue is full
|
||||
cv_full.wait(lock, [this]() { return !running || !is_full(); });
|
||||
|
||||
if (!running) {
|
||||
// discard messages while the worker thread is paused
|
||||
return;
|
||||
}
|
||||
|
||||
auto & entry = queue[tail];
|
||||
|
||||
if (entry.msg.size() < text.size() + 1) {
|
||||
entry.msg.resize(text.size() + 1);
|
||||
}
|
||||
memcpy(entry.msg.data(), text.c_str(), text.size() + 1);
|
||||
|
||||
entry.is_end = false;
|
||||
entry.level = GGML_LOG_LEVEL_NONE;
|
||||
entry.prefix = false;
|
||||
entry.jsonl = true;
|
||||
entry.is_json = true;
|
||||
entry.timestamp = 0;
|
||||
|
||||
tail = (tail + 1) % queue.size();
|
||||
cv_new.notify_one();
|
||||
}
|
||||
|
||||
void resume() {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
@@ -388,12 +440,6 @@ public:
|
||||
|
||||
this->timestamps = timestamps;
|
||||
}
|
||||
|
||||
void set_jsonl(bool jsonl) {
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
this->jsonl = jsonl;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
@@ -440,6 +486,14 @@ void common_log_add(struct common_log * log, enum ggml_log_level level, const ch
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void common_log_add_json(struct common_log * log, const char * type, const common_json & obj) {
|
||||
if (!common_log_jsonl) {
|
||||
return;
|
||||
}
|
||||
|
||||
log->add_json(type, obj);
|
||||
}
|
||||
|
||||
void common_log_set_file(struct common_log * log, const char * file) {
|
||||
log->set_file(file);
|
||||
}
|
||||
@@ -467,10 +521,6 @@ void common_log_set_timestamps(struct common_log * log, bool timestamps) {
|
||||
log->set_timestamps(timestamps);
|
||||
}
|
||||
|
||||
void common_log_set_jsonl(struct common_log * log, bool jsonl) {
|
||||
log->set_jsonl(jsonl);
|
||||
}
|
||||
|
||||
void common_log_flush(struct common_log * log) {
|
||||
log->pause();
|
||||
log->resume();
|
||||
|
||||
+18
-1
@@ -43,6 +43,10 @@ int common_log_get_verbosity_thold(void);
|
||||
|
||||
void common_log_set_verbosity_thold(int verbosity); // not thread-safe
|
||||
|
||||
bool common_log_get_jsonl(void);
|
||||
|
||||
void common_log_set_jsonl(bool jsonl); // not thread-safe
|
||||
|
||||
int common_log_get_verbosity(enum ggml_log_level level);
|
||||
|
||||
void common_log_default_callback(enum ggml_log_level level, const char * text, void * user_data);
|
||||
@@ -91,7 +95,6 @@ void common_log_set_file (struct common_log * log, const char * file); // n
|
||||
void common_log_set_colors (struct common_log * log, log_colors colors); // not thread-safe
|
||||
void common_log_set_prefix (struct common_log * log, bool prefix); // whether to output prefix to each log
|
||||
void common_log_set_timestamps(struct common_log * log, bool timestamps); // whether to output timestamps in the prefix
|
||||
void common_log_set_jsonl (struct common_log * log, bool jsonl); // print each log as a JSON object on one line, not thread-safe
|
||||
void common_log_flush (struct common_log * log); // flush all pending log messages
|
||||
|
||||
// helper macros for logging
|
||||
@@ -127,3 +130,17 @@ void common_log_flush (struct common_log * log); // f
|
||||
#define LOG_WRNV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_WARN, verbosity, __VA_ARGS__)
|
||||
#define LOG_ERRV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_ERROR, verbosity, __VA_ARGS__)
|
||||
#define LOG_CNTV(verbosity, ...) LOG_TMPL(GGML_LOG_LEVEL_CONT, verbosity, __VA_ARGS__)
|
||||
|
||||
class common_json; // defined in common/json.h
|
||||
|
||||
// helper allows different types of json output
|
||||
// no-op if --log-jsonl is not set
|
||||
void common_log_add_json(struct common_log * log, const char * type, const common_json & data);
|
||||
|
||||
// will only print if --log-jsonl is set
|
||||
#define LOG_JSON(type, data) \
|
||||
do { \
|
||||
if (common_log_get_jsonl()) { \
|
||||
common_log_add_json(common_log_main(), type, data); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -129,15 +129,6 @@ common_chat_params common_chat_params_init_cohere2moe(const common_chat_template
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -149,39 +149,28 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
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();
|
||||
|
||||
std::set<std::string> required;
|
||||
if (params.contains("required")) {
|
||||
required = params.at("required").get<std::set<std::string>>();
|
||||
}
|
||||
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
|
||||
std::vector<common_peg_parser> required_parsers;
|
||||
std::vector<common_peg_parser> optional_parsers;
|
||||
for (const auto & [param_name, param_schema] : props.items()) {
|
||||
bool is_required = required.find(param_name) != required.end();
|
||||
bool is_string = schema_info.resolves_to_string(param_schema);
|
||||
foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
|
||||
bool is_string = param.schema->may_be_string();
|
||||
|
||||
auto arg = p.tool_arg(
|
||||
p.tool_arg_open(p.literal(PARAM_START + " name=\"") + p.tool_arg_name(p.literal(param_name)) +
|
||||
p.tool_arg_open(p.literal(PARAM_START + " name=\"") + p.tool_arg_name(p.literal(param.name)) +
|
||||
p.literal("\" string=\"" + std::string(is_string ? "true" : "false") + "\">")) +
|
||||
(is_string ?
|
||||
p.tool_arg_string_value(p.until(PARAM_END)) :
|
||||
p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param_name + "-schema",
|
||||
param_schema, false))) +
|
||||
p.tool_arg_json_value(p.schema(p.json(), "tool-" + name + "-arg-" + param.name + "-schema",
|
||||
doc, *param.schema))) +
|
||||
p.tool_arg_close(p.literal(PARAM_END)));
|
||||
|
||||
auto named_arg = p.rule("tool-" + name + "-arg-" + param_name, arg);
|
||||
if (is_required) {
|
||||
auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg);
|
||||
if (param.required) {
|
||||
required_parsers.push_back(named_arg);
|
||||
} else {
|
||||
optional_parsers.push_back(named_arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
common_peg_parser args_seq = p.eps();
|
||||
for (size_t i = 0; i < required_parsers.size(); i++) {
|
||||
@@ -266,15 +255,6 @@ common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_templ
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = has_tools && !require_tools;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & schema = function.at("parameters");
|
||||
const auto schema = common_chat_tool_parameters(function);
|
||||
|
||||
// Tool format: >>>function_name\n{json_args}
|
||||
auto tool_parser = p.tool(
|
||||
@@ -82,11 +82,6 @@ common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_te
|
||||
data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -291,15 +291,6 @@ common_chat_params common_chat_params_init_gemma4(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ common_chat_params common_chat_params_init_gigachat_v3(
|
||||
for (const auto & tool : inputs.tools) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & schema = function.at("parameters");
|
||||
const auto schema = common_chat_tool_parameters(function);
|
||||
|
||||
auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\"");
|
||||
auto tool_args = p.json_member("arguments", p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)));
|
||||
@@ -65,11 +65,6 @@ common_chat_params common_chat_params_init_gigachat_v3(
|
||||
data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & params = function.at("parameters");
|
||||
const auto params = common_chat_tool_parameters(function);
|
||||
|
||||
auto func_name = p.literal(" to=functions.") + p.tool_name(p.literal(name));
|
||||
auto constraint = p.optional(p.space() + p.optional(p.literal("<|constrain|>")) + constrain_type);
|
||||
@@ -143,15 +143,6 @@ common_chat_params common_chat_params_init_gpt_oss(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template &
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & schema = function.at("parameters");
|
||||
const auto schema = common_chat_tool_parameters(function);
|
||||
|
||||
// Match: functions.<name>:<digits>
|
||||
// Capture the full call id (functions.<name>:<digits>) using tool_id tag
|
||||
@@ -116,11 +116,6 @@ common_chat_params common_chat_params_init_kimi_k2(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template &
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const json schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
const json schema = common_chat_tool_parameters(function);
|
||||
|
||||
// arguments come one tag per key, with the JSON type in a type="..."
|
||||
// attribute. the type is taken from the tool schema instead, as it tells
|
||||
@@ -155,13 +155,6 @@ common_chat_params common_chat_params_init_kimi_k3(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
if (function.contains("parameters")) {
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -98,15 +98,6 @@ common_chat_params common_chat_params_init_lfm2(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
+19
-33
@@ -71,32 +71,27 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template &
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
const std::string name = function.at("name");
|
||||
auto params = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
|
||||
std::vector<common_peg_parser> arg_rules;
|
||||
foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) {
|
||||
auto value_parser = p.eps();
|
||||
if (prop.schema->may_be_string()) {
|
||||
value_parser = string_value;
|
||||
} else {
|
||||
value_parser = p.tool_arg_json_value(
|
||||
p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema)
|
||||
) + p.tool_arg_close(p.literal("</param>"));
|
||||
}
|
||||
|
||||
arg_rules.push_back(p.tool_arg(
|
||||
p.tool_arg_open(p.literal("<param name=\"") + p.tool_arg_name(p.literal(prop.name)) + p.literal("\">")) +
|
||||
value_parser
|
||||
));
|
||||
});
|
||||
|
||||
auto args = p.eps();
|
||||
if (params.contains("properties") && params.at("properties").is_object() && !params.at("properties").empty()) {
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
|
||||
auto arg_choice = p.choice();
|
||||
for (const auto & [prop_name, prop_schema] : params.at("properties").items()) {
|
||||
auto value_parser = p.eps();
|
||||
if (schema_info.resolves_to_string(prop_schema)) {
|
||||
value_parser = string_value;
|
||||
} else {
|
||||
value_parser = p.tool_arg_json_value(
|
||||
p.schema(p.json(), "tool-" + name + "-arg-" + prop_name + "-schema", prop_schema, false)
|
||||
) + p.tool_arg_close(p.literal("</param>"));
|
||||
}
|
||||
|
||||
auto arg_rule = p.tool_arg(
|
||||
p.tool_arg_open(p.literal("<param name=\"") + p.tool_arg_name(p.literal(prop_name)) + p.literal("\">")) +
|
||||
value_parser
|
||||
);
|
||||
|
||||
arg_choice |= arg_rule;
|
||||
}
|
||||
args = p.zero_or_more(arg_choice + p.space());
|
||||
if (!arg_rules.empty()) {
|
||||
args = p.zero_or_more(p.choice(arg_rules) + p.space());
|
||||
}
|
||||
|
||||
auto tool_parser = p.tool(
|
||||
@@ -123,15 +118,6 @@ common_chat_params common_chat_params_init_minicpm5(const common_chat_template &
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -84,29 +84,18 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template
|
||||
return generation_prompt + reasoning + p.content(p.rest()) + end;
|
||||
}
|
||||
|
||||
auto alternatives_of = [](const json & schema) -> std::optional<json> {
|
||||
for (const auto * keyword : { "oneOf", "anyOf" }) {
|
||||
if (schema.contains(keyword) && schema.at(keyword).is_array() && !schema.at(keyword).empty()) {
|
||||
return schema.at(keyword);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto tool_choice = p.choice();
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
auto params = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
auto params = common_chat_tool_parameters(function);
|
||||
auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(params));
|
||||
|
||||
// The template expands argument values recursively in XML (see the to_xml() macro)
|
||||
std::function<common_peg_parser(const json &, const std::string &, const std::string &)> value_of;
|
||||
std::function<common_peg_parser(const json &, const std::string &)> members_of;
|
||||
std::function<common_peg_parser(const common_chat_schema &, const std::string &, const std::string &)> value_of;
|
||||
std::function<common_peg_parser(const common_chat_schema_object &, const std::string &)> members_of;
|
||||
|
||||
auto element_of = [&](const std::string & tag, const json & schema, const std::string & rule_name) {
|
||||
auto element_of = [&](const std::string & tag, const common_chat_schema & schema, const std::string & rule_name) {
|
||||
const std::string close = NS + "</" + tag + ">";
|
||||
return p.rule(rule_name,
|
||||
p.tool_arg(
|
||||
@@ -117,69 +106,57 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template
|
||||
value_of(schema, rule_name, close)));
|
||||
};
|
||||
|
||||
value_of = [&](const json & schema,
|
||||
value_of = [&](const common_chat_schema & schema,
|
||||
const std::string & rule_name,
|
||||
const std::string & close) -> common_peg_parser {
|
||||
auto close_tag = p.tool_arg_close(p.literal(close));
|
||||
|
||||
// A string accepts anything, so a union with a string alternative is a string
|
||||
if (schema_info.resolves_to_string(schema)) {
|
||||
if (schema.may_be_string()) {
|
||||
return p.ac(p.tool_arg_string_value(p.until(close)) + close_tag, close);
|
||||
}
|
||||
|
||||
if (auto alternatives = alternatives_of(schema)) {
|
||||
if (schema.kind() == common_chat_schema::KIND_ANY_OF) {
|
||||
std::vector<common_peg_parser> choices;
|
||||
|
||||
size_t index = 0;
|
||||
for (const auto & alternative : *alternatives) {
|
||||
for (const auto & alternative : static_cast<const common_chat_schema_any_of &>(schema).children) {
|
||||
const std::string alt_name = rule_name + "-" + std::to_string(index++);
|
||||
|
||||
// There is a risk that this breaks streaming deltas, but that's a risk we
|
||||
// assume to provide tool arg streaming.
|
||||
choices.push_back(value_of(alternative, alt_name, close));
|
||||
choices.push_back(value_of(*alternative, alt_name, close));
|
||||
}
|
||||
|
||||
return p.choice(choices);
|
||||
}
|
||||
|
||||
const std::string type = schema.contains("type") && schema.at("type").is_string()
|
||||
? schema.at("type").get<std::string>()
|
||||
: "";
|
||||
|
||||
if (type == "object" && schema.contains("properties")) {
|
||||
return p.tag(mm3::TOOL_ARG_OBJECT, members_of(schema, rule_name)) + p.space() + close_tag;
|
||||
if (schema.kind() == common_chat_schema::KIND_OBJECT) {
|
||||
const auto & object = static_cast<const common_chat_schema_object &>(schema);
|
||||
if (!object.properties.empty()) {
|
||||
return p.tag(mm3::TOOL_ARG_OBJECT, members_of(object, rule_name)) + p.space() + close_tag;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "array" && schema.contains("items")) {
|
||||
if (schema.kind() == common_chat_schema::KIND_ARRAY) {
|
||||
const std::string item_close = NS + "</item>";
|
||||
auto item = p.rule(rule_name + "-item",
|
||||
p.tag(mm3::TOOL_ARG_ITEM,
|
||||
p.literal(NS + "<item>") +
|
||||
value_of(schema.at("items"), rule_name + "-item", item_close)));
|
||||
value_of(*static_cast<const common_chat_schema_array &>(schema).items, rule_name + "-item", item_close)));
|
||||
return p.tag(mm3::TOOL_ARG_ARRAY, p.repeat(p.space() + item, 0, -1)) + p.space() + close_tag;
|
||||
}
|
||||
|
||||
return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", schema, false)) + close_tag;
|
||||
return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, schema)) + close_tag;
|
||||
};
|
||||
|
||||
// Required properties in schema order, then any number of optional ones in any order.
|
||||
members_of = [&](const json & schema, const std::string & rule_prefix) -> common_peg_parser {
|
||||
const auto & props = schema.at("properties");
|
||||
|
||||
std::set<std::string> required;
|
||||
if (schema.contains("required")) {
|
||||
required = schema.at("required").get<std::set<std::string>>();
|
||||
}
|
||||
|
||||
members_of = [&](const common_chat_schema_object & object, const std::string & rule_prefix) -> common_peg_parser {
|
||||
std::vector<common_peg_parser> required_elements;
|
||||
std::vector<common_peg_parser> optional_elements;
|
||||
for (const auto & [key, key_schema] : props.items()) {
|
||||
auto element = element_of(key, key_schema, rule_prefix + "-" + key);
|
||||
if (required.find(key) != required.end()) {
|
||||
required_elements.push_back(element);
|
||||
} else {
|
||||
optional_elements.push_back(element);
|
||||
}
|
||||
for (const auto & prop : object.properties) {
|
||||
auto element = element_of(prop.name, *prop.schema, rule_prefix + "-" + prop.name);
|
||||
(prop.required ? required_elements : optional_elements).push_back(element);
|
||||
}
|
||||
|
||||
common_peg_parser members = p.eps();
|
||||
@@ -201,8 +178,10 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template
|
||||
return members;
|
||||
};
|
||||
|
||||
common_peg_parser invoke_body =
|
||||
params.contains("properties") ? members_of(params, "tool-" + name + "-arg") : p.eps();
|
||||
common_peg_parser invoke_body = p.eps();
|
||||
if (doc->root->kind() == common_chat_schema::KIND_OBJECT) {
|
||||
invoke_body = members_of(static_cast<const common_chat_schema_object &>(*doc->root), "tool-" + name + "-arg");
|
||||
}
|
||||
|
||||
auto func_parser = p.tool(
|
||||
p.tool_open(p.literal(NS + "<invoke name=\"") +
|
||||
@@ -238,15 +217,6 @@ common_chat_params common_chat_params_init_minimax_m3(const common_chat_template
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & schema = function.at("parameters");
|
||||
const auto schema = common_chat_tool_parameters(function);
|
||||
|
||||
tool_choice |=
|
||||
p.rule("tool-" + name, p.tool_open(p.tool_name(p.literal(name)) + "[ARGS]") +
|
||||
@@ -114,15 +114,6 @@ common_chat_params common_chat_params_init_ministral_3(const common_chat_templat
|
||||
data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.at("parameters");
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
@@ -74,31 +74,26 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
const std::string name = function.at("name");
|
||||
auto params = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
|
||||
std::vector<common_peg_parser> arg_rules;
|
||||
foreach_parameter(function, [&](const common_chat_schema_property & prop, const common_chat_schema_document_ptr & doc) {
|
||||
auto value_parser = p.eps();
|
||||
if (prop.schema->may_be_string()) {
|
||||
value_parser = string_value;
|
||||
} else {
|
||||
value_parser = p.tool_arg_json_value(
|
||||
p.schema(p.json(), "tool-" + name + "-arg-" + prop.name + "-schema", doc, *prop.schema))
|
||||
+ p.tool_arg_close(p.literal("</atem:parameter>"));
|
||||
}
|
||||
|
||||
arg_rules.push_back(p.tool_arg(
|
||||
p.tool_arg_open(p.literal("<atem:parameter name=\"") + p.tool_arg_name(p.literal(prop.name)) + p.literal("\">")) +
|
||||
value_parser));
|
||||
});
|
||||
|
||||
auto args = p.eps();
|
||||
if (params.contains("properties") && params.at("properties").is_object() && !params.at("properties").empty()) {
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
|
||||
auto arg_choice = p.choice();
|
||||
for (const auto & [prop_name, prop_schema] : params.at("properties").items()) {
|
||||
auto value_parser = p.eps();
|
||||
if (schema_info.resolves_to_string(prop_schema)) {
|
||||
value_parser = string_value;
|
||||
} else {
|
||||
value_parser = p.tool_arg_json_value(
|
||||
p.schema(p.json(), "tool-" + name + "-arg-" + prop_name + "-schema", prop_schema, false))
|
||||
+ p.tool_arg_close(p.literal("</atem:parameter>"));
|
||||
}
|
||||
|
||||
auto arg_rule = p.tool_arg(
|
||||
p.tool_arg_open(p.literal("<atem:parameter name=\"") + p.tool_arg_name(p.literal(prop_name)) + p.literal("\">")) +
|
||||
value_parser);
|
||||
|
||||
arg_choice |= arg_rule;
|
||||
}
|
||||
args = p.zero_or_more(arg_choice + p.space());
|
||||
if (!arg_rules.empty()) {
|
||||
args = p.zero_or_more(p.choice(arg_rules) + p.space());
|
||||
}
|
||||
|
||||
auto tool_parser = p.tool(
|
||||
@@ -131,11 +126,6 @@ common_chat_params common_chat_params_init_muse_glimmer(const common_chat_templa
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
data.grammar_triggers = {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include "log.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
void foreach_function(const json & tools, const std::function<void(const json &)> & fn) {
|
||||
for (const auto & tool : tools) {
|
||||
if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) {
|
||||
@@ -14,21 +12,14 @@ void foreach_function(const json & tools, const std::function<void(const json &)
|
||||
}
|
||||
}
|
||||
|
||||
void foreach_parameter(const json & function, const std::function<void(const std::string &, const json &, bool)> & fn) {
|
||||
if (!function.contains("parameters") || !function.at("parameters").is_object()) {
|
||||
void foreach_parameter(const json & function, const std::function<void(const common_chat_schema_property &, const common_chat_schema_document_ptr &)> & fn) {
|
||||
auto params = common_chat_tool_parameters(function);
|
||||
auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(params));
|
||||
const auto * object = dynamic_cast<const common_chat_schema_object *>(doc->root.get());
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
const auto & params = function.at("parameters");
|
||||
if (!params.contains("properties") || !params.at("properties").is_object()) {
|
||||
return;
|
||||
}
|
||||
const auto & props = params.at("properties");
|
||||
std::set<std::string> required;
|
||||
if (params.contains("required") && params.at("required").is_array()) {
|
||||
required = params.at("required").get<std::set<std::string>>();
|
||||
}
|
||||
for (const auto & [name, prop] : props.items()) {
|
||||
bool is_required = (required.find(name) != required.end());
|
||||
fn(name, prop, is_required);
|
||||
for (const auto & prop : object->properties) {
|
||||
fn(prop, doc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ using json = common_json;
|
||||
// iterate over the function tools of an OpenAI-style tools array
|
||||
void foreach_function(const json & tools, const std::function<void(const json &)> & fn);
|
||||
|
||||
// iterate over the parameters of a function tool, flagging the ones listed as required
|
||||
void foreach_parameter(const json & function, const std::function<void(const std::string &, const json &, bool)> & fn);
|
||||
// iterate over the parameters of a function tool, with the document that owns them
|
||||
void foreach_parameter(const json & function, const std::function<void(const common_chat_schema_property &, const common_chat_schema_document_ptr &)> & fn);
|
||||
|
||||
// render a template; the override arguments let a parser feed in messages, tools or context it has rewritten
|
||||
std::string common_chat_template_direct_apply_impl(
|
||||
|
||||
@@ -93,28 +93,49 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat
|
||||
|
||||
auto tool_choice = p.choice();
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
auto parameters = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(parameters);
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
|
||||
std::vector<common_peg_parser> required_args;
|
||||
std::vector<common_peg_parser> optional_args;
|
||||
|
||||
foreach_parameter(function, [&](const std::string & param_name, const json & param_schema, bool is_required) {
|
||||
auto rule_name = "tool-" + name + "-arg-" + param_name;
|
||||
foreach_parameter(function, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {
|
||||
auto rule_name = "tool-" + name + "-arg-" + param.name;
|
||||
|
||||
auto arg_open = p.tool_arg_open("<parameter=" + p.tool_arg_name(p.literal(param_name)) + ">\n");
|
||||
auto arg_open = p.tool_arg_open("<parameter=" + p.tool_arg_name(p.literal(param.name)) + ">\n");
|
||||
|
||||
auto arg_value = schema_info.resolves_to_string(param_schema) ?
|
||||
arg_string :
|
||||
p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", param_schema)) + arg_close;
|
||||
auto types = param.schema->value_types();
|
||||
|
||||
auto arg_value = p.eps();
|
||||
if (!types.has(common_chat_schema::TYPE_STRING)) {
|
||||
arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, *param.schema)) + arg_close;
|
||||
} else if (types.is_only(common_chat_schema::TYPE_STRING)) {
|
||||
arg_value = arg_string;
|
||||
} else {
|
||||
// The string alternative accepts any text, so the grammar only keeps the raw string
|
||||
// rule. The parser still tries the JSON alternatives first to type the value.
|
||||
auto json_value = p.choice();
|
||||
if (types.has(common_chat_schema::TYPE_OBJECT)) {
|
||||
json_value |= p.json_object();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_ARRAY)) {
|
||||
json_value |= p.json_array();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_NUMBER) || types.has(common_chat_schema::TYPE_INTEGER)) {
|
||||
json_value |= p.json_number();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_BOOLEAN)) {
|
||||
json_value |= p.json_bool();
|
||||
}
|
||||
if (types.has(common_chat_schema::TYPE_NULL)) {
|
||||
json_value |= p.json_null();
|
||||
}
|
||||
arg_value = p.gbnf(p.atomic(p.tool_arg_json_value(json_value) + arg_close) | arg_string, "xml-arg-string");
|
||||
}
|
||||
|
||||
auto arg_rule = p.rule(rule_name, p.tool_arg(arg_open + arg_value));
|
||||
|
||||
(is_required ? required_args : optional_args).push_back(arg_rule);
|
||||
(param.required ? required_args : optional_args).push_back(arg_rule);
|
||||
});
|
||||
|
||||
// Accept required arguments in any order, as Qwen does not always adhere to the
|
||||
@@ -158,15 +179,6 @@ common_chat_params common_chat_params_init_qwen3_coder(const common_chat_templat
|
||||
data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
|
||||
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
auto schema = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
|
||||
+11
-31
@@ -953,7 +953,7 @@ std::string common_peg_arena::dump_impl(common_peg_parser_id
|
||||
} else if constexpr (std::is_same_v<T, common_peg_until_parser>) {
|
||||
return "Until(" + string_join(p.delimiters, " | ") + ")";
|
||||
} else if constexpr (std::is_same_v<T, common_peg_schema_parser>) {
|
||||
return "Schema(" + dump_impl(p.child, visited) + ", " + (p.schema ? p.schema->dump() : "null") + ")";
|
||||
return "Schema(" + dump_impl(p.child, visited) + ", " + (p.node ? common_chat_schema::kind_name(p.node->kind()) : "null") + ")";
|
||||
} else if constexpr (std::is_same_v<T, common_peg_rule_parser>) {
|
||||
return "Rule(" + p.name + ", " + dump_impl(p.child, visited) + ")";
|
||||
} else if constexpr (std::is_same_v<T, common_peg_ref_parser>) {
|
||||
@@ -1119,8 +1119,13 @@ 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, common_chat_schema_document_ptr doc, const common_chat_schema & node, bool raw) {
|
||||
return wrap(arena_.add_parser(common_peg_schema_parser{p.id(), name, std::move(doc), &node, 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<common_json>(schema), raw}));
|
||||
auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(schema));
|
||||
return this->schema(p, name, doc, *doc->root, raw);
|
||||
}
|
||||
|
||||
common_peg_parser common_peg_parser_builder::rule(const std::string & name, const common_peg_parser & p, bool trigger) {
|
||||
@@ -1573,30 +1578,9 @@ static std::set<std::string> collect_reachable_rules(
|
||||
|
||||
// GBNF generation implementation
|
||||
void common_peg_arena::build_grammar(const common_grammar_builder & builder, bool lazy) const {
|
||||
// A raw string value is parsed by the child rather than constrained by the schema
|
||||
auto schema_delegates = [](const common_peg_schema_parser & s) -> bool {
|
||||
if (!s.schema) {
|
||||
return true;
|
||||
}
|
||||
if (s.raw && s.schema->contains("type")) {
|
||||
const auto & type_val = s.schema->at("type");
|
||||
if (type_val.is_string() && type_val == "string") {
|
||||
return true;
|
||||
}
|
||||
// Handle nullable types like ["string", "null"] - delegate when the
|
||||
// non-null type is string, since the tagged format uses raw text
|
||||
if (type_val.is_array()) {
|
||||
for (const auto & t : type_val) {
|
||||
if (t.is_string() && t.get<std::string>() != "null") {
|
||||
return t.get<std::string>() == "string";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delegate for enum schemas in raw mode - enum values are literal strings
|
||||
if (s.raw && !s.schema->contains("type") && s.schema->contains("enum")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !s.node || (s.raw && s.node->may_be_string());
|
||||
};
|
||||
|
||||
// Unwrap the parser so we can properly check if it's a sequence or choice
|
||||
@@ -1731,7 +1715,7 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo
|
||||
if (schema_delegates(p)) {
|
||||
return to_gbnf(p.child);
|
||||
}
|
||||
return builder.add_schema(p.name, *p.schema);
|
||||
return builder.add_schema(p.name, *p.node);
|
||||
} else if constexpr (std::is_same_v<T, common_peg_rule_parser>) {
|
||||
return p.name;
|
||||
} else if constexpr (std::is_same_v<T, common_peg_ref_parser>) {
|
||||
@@ -1859,7 +1843,6 @@ static common_json serialize_parser_variant(const common_peg_parser_variant & va
|
||||
{"type", "schema"},
|
||||
{"child", p.child},
|
||||
{"name", p.name},
|
||||
{"schema", p.schema ? *p.schema : json(nullptr)},
|
||||
{"raw", p.raw}
|
||||
};
|
||||
} else if constexpr (std::is_same_v<T, common_peg_rule_parser>) {
|
||||
@@ -1999,15 +1982,12 @@ static common_peg_parser_variant deserialize_parser_variant(const common_json &
|
||||
return common_peg_until_parser{j["delimiters"].get<std::vector<std::string>>()};
|
||||
}
|
||||
if (type == "schema") {
|
||||
if (!j.contains("child") || !j.contains("name") || !j.contains("schema") || !j.contains("raw")) {
|
||||
if (!j.contains("child") || !j.contains("name") || !j.contains("raw")) {
|
||||
throw std::runtime_error("schema parser missing required fields");
|
||||
}
|
||||
common_peg_schema_parser parser;
|
||||
parser.child = j["child"].get<common_peg_parser_id>();
|
||||
parser.name = j["name"];
|
||||
if (!j["schema"].is_null()) {
|
||||
parser.schema = std::make_shared<common_json>(j["schema"]);
|
||||
}
|
||||
parser.raw = j["raw"].get<bool>();
|
||||
return parser;
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "json-schema.h"
|
||||
#include "json.h"
|
||||
|
||||
#include <memory>
|
||||
@@ -245,7 +246,8 @@ struct common_peg_until_parser {
|
||||
struct common_peg_schema_parser {
|
||||
common_peg_parser_id child;
|
||||
std::string name;
|
||||
std::shared_ptr<common_json> schema;
|
||||
common_chat_schema_document_ptr doc; // owns node
|
||||
const common_chat_schema * node = nullptr;
|
||||
|
||||
// Indicates if the GBNF should accept a raw string that matches the schema.
|
||||
bool raw;
|
||||
@@ -488,8 +490,10 @@ class common_peg_parser_builder {
|
||||
// A marker, i.e. text delimited by a pair of <> or []
|
||||
common_peg_parser marker();
|
||||
|
||||
// Wraps a parser with JSON schema metadata for grammar generation.
|
||||
// Used internally to convert JSON schemas to GBNF grammar rules.
|
||||
// Wraps a parser with the schema its GBNF is generated from, a node of the document that owns it
|
||||
common_peg_parser schema(const common_peg_parser & p, const std::string & name, common_chat_schema_document_ptr doc, const common_chat_schema & node, bool raw = false);
|
||||
|
||||
// Parses the JSON schema into a document of its own
|
||||
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.
|
||||
|
||||
@@ -28,7 +28,7 @@ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
||||
for (const auto & tool : tools) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
const auto & schema = function.at("parameters");
|
||||
const auto schema = common_chat_tool_parameters(function);
|
||||
|
||||
auto tool_name = p.json_member("name", "\"" + p.literal(name) + "\"");
|
||||
auto tool_args = p.json_member("arguments", p.schema(p.json(), "tool-" + name + "-schema", schema));
|
||||
@@ -108,6 +108,7 @@ For a more complete example, see `test_example_native()` in
|
||||
- **`rule(name, p, trigger)`** - Creates a named rule and returns a reference
|
||||
- **`trigger_rule(name, p)`** - Creates a trigger rule (entry point for lazy grammar generation)
|
||||
- **`schema(p, name, schema, raw)`** - Wraps parser with JSON schema metadata for grammar generation
|
||||
- **`schema(p, name, doc, node, raw)`** - Same, for a node of a `common_chat_schema_document` built earlier, e.g. one tool parameter
|
||||
|
||||
### AST Control
|
||||
|
||||
@@ -121,9 +122,6 @@ some exceptions.
|
||||
|
||||
```cpp
|
||||
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
foreach_function(params.tools, [&](const json & fn) {
|
||||
builder.resolve_refs(fn.at("parameters"));
|
||||
});
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
```
|
||||
@@ -151,7 +149,8 @@ implementation to generate the grammar instead of the underlying parser.
|
||||
|
||||
The `raw` option emits a grammar suitable for a raw string instead of a JSON
|
||||
string. In other words, it won't be wrapped in quotes or require escaping
|
||||
quotes. It should only be used when `type == "string"`.
|
||||
quotes. It only takes effect when the schema may be a string, as reported by
|
||||
`common_chat_schema::may_be_string()`, otherwise the JSON grammar is used.
|
||||
|
||||
The downside is that it can potentially lead to ambiguous grammars. For
|
||||
example, if a user provides the pattern `^.*$`, the following grammar may be
|
||||
|
||||
@@ -1,842 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any, List, Optional, Set, Tuple, Union
|
||||
|
||||
def _build_repetition(item_rule, min_items, max_items, separator_rule=None):
|
||||
|
||||
if max_items == 0:
|
||||
return ""
|
||||
|
||||
if min_items == 0 and max_items == 1:
|
||||
return f'{item_rule}?'
|
||||
|
||||
if not separator_rule:
|
||||
if min_items == 1 and max_items is None:
|
||||
return f'{item_rule}+'
|
||||
elif min_items == 0 and max_items is None:
|
||||
return f'{item_rule}*'
|
||||
else:
|
||||
return f'{item_rule}{{{min_items},{max_items if max_items is not None else ""}}}'
|
||||
|
||||
result = item_rule + ' ' + _build_repetition(f'({separator_rule} {item_rule})', min_items - 1 if min_items > 0 else 0, max_items - 1 if max_items is not None else None)
|
||||
return f'({result})?' if min_items == 0 else result
|
||||
|
||||
def _generate_min_max_int(min_value: Optional[int], max_value: Optional[int], out: list, decimals_left: int = 16, top_level: bool = True):
|
||||
def digit_range(from_char: str, to_char: str):
|
||||
out.append("[")
|
||||
if from_char == to_char:
|
||||
out.append(from_char)
|
||||
else:
|
||||
out.append(from_char)
|
||||
out.append("-")
|
||||
out.append(to_char)
|
||||
out.append("]")
|
||||
|
||||
def more_digits(min_digits: int, max_digits: int):
|
||||
out.append("[0-9]")
|
||||
if min_digits == max_digits and min_digits == 1:
|
||||
return
|
||||
out.append("{")
|
||||
out.append(str(min_digits))
|
||||
if max_digits != min_digits:
|
||||
out.append(",")
|
||||
if max_digits != sys.maxsize:
|
||||
out.append(str(max_digits))
|
||||
out.append("}")
|
||||
|
||||
def uniform_range(from_str: str, to_str: str):
|
||||
i = 0
|
||||
while i < len(from_str) and from_str[i] == to_str[i]:
|
||||
i += 1
|
||||
if i > 0:
|
||||
out.append("\"")
|
||||
out.append(from_str[:i])
|
||||
out.append("\"")
|
||||
if i < len(from_str):
|
||||
if i > 0:
|
||||
out.append(" ")
|
||||
sub_len = len(from_str) - i - 1
|
||||
if sub_len > 0:
|
||||
from_sub = from_str[i+1:]
|
||||
to_sub = to_str[i+1:]
|
||||
sub_zeros = "0" * sub_len
|
||||
sub_nines = "9" * sub_len
|
||||
|
||||
to_reached = False
|
||||
out.append("(")
|
||||
if from_sub == sub_zeros:
|
||||
digit_range(from_str[i], chr(ord(to_str[i]) - 1))
|
||||
out.append(" ")
|
||||
more_digits(sub_len, sub_len)
|
||||
else:
|
||||
out.append("[")
|
||||
out.append(from_str[i])
|
||||
out.append("] ")
|
||||
out.append("(")
|
||||
uniform_range(from_sub, sub_nines)
|
||||
out.append(")")
|
||||
if ord(from_str[i]) < ord(to_str[i]) - 1:
|
||||
out.append(" | ")
|
||||
if to_sub == sub_nines:
|
||||
digit_range(chr(ord(from_str[i]) + 1), to_str[i])
|
||||
to_reached = True
|
||||
else:
|
||||
digit_range(chr(ord(from_str[i]) + 1), chr(ord(to_str[i]) - 1))
|
||||
out.append(" ")
|
||||
more_digits(sub_len, sub_len)
|
||||
if not to_reached:
|
||||
out.append(" | ")
|
||||
digit_range(to_str[i], to_str[i])
|
||||
out.append(" ")
|
||||
uniform_range(sub_zeros, to_sub)
|
||||
out.append(")")
|
||||
else:
|
||||
out.append("[")
|
||||
out.append(from_str[i])
|
||||
out.append("-")
|
||||
out.append(to_str[i])
|
||||
out.append("]")
|
||||
|
||||
if min_value is not None and max_value is not None:
|
||||
if min_value < 0 and max_value < 0:
|
||||
out.append("\"-\" (")
|
||||
_generate_min_max_int(-max_value, -min_value, out, decimals_left, top_level=True)
|
||||
out.append(")")
|
||||
return
|
||||
|
||||
if min_value < 0:
|
||||
out.append("\"-\" (")
|
||||
_generate_min_max_int(0, -min_value, out, decimals_left, top_level=True)
|
||||
out.append(") | ")
|
||||
min_value = 0
|
||||
|
||||
min_s = str(min_value)
|
||||
max_s = str(max_value)
|
||||
min_digits = len(min_s)
|
||||
max_digits = len(max_s)
|
||||
|
||||
for digits in range(min_digits, max_digits):
|
||||
uniform_range(min_s, "9" * digits)
|
||||
min_s = "1" + "0" * digits
|
||||
out.append(" | ")
|
||||
uniform_range(min_s, max_s)
|
||||
return
|
||||
|
||||
less_decimals = max(decimals_left - 1, 1)
|
||||
|
||||
if min_value is not None:
|
||||
if min_value < 0:
|
||||
out.append("\"-\" (")
|
||||
_generate_min_max_int(None, -min_value, out, decimals_left, top_level=False)
|
||||
out.append(") | [0] | [1-9] ")
|
||||
more_digits(0, decimals_left - 1)
|
||||
elif min_value == 0:
|
||||
if top_level:
|
||||
out.append("[0] | [1-9] ")
|
||||
more_digits(0, less_decimals)
|
||||
else:
|
||||
more_digits(1, decimals_left)
|
||||
elif min_value <= 9:
|
||||
c = str(min_value)
|
||||
range_start = '1' if top_level else '0'
|
||||
if c > range_start:
|
||||
digit_range(range_start, chr(ord(c) - 1))
|
||||
out.append(" ")
|
||||
more_digits(1, less_decimals)
|
||||
out.append(" | ")
|
||||
digit_range(c, "9")
|
||||
out.append(" ")
|
||||
more_digits(0, less_decimals)
|
||||
else:
|
||||
min_s = str(min_value)
|
||||
length = len(min_s)
|
||||
c = min_s[0]
|
||||
|
||||
if c > "1":
|
||||
digit_range("1" if top_level else "0", chr(ord(c) - 1))
|
||||
out.append(" ")
|
||||
more_digits(length, less_decimals)
|
||||
out.append(" | ")
|
||||
digit_range(c, c)
|
||||
out.append(" (")
|
||||
_generate_min_max_int(int(min_s[1:]), None, out, less_decimals, top_level=False)
|
||||
out.append(")")
|
||||
if c < "9":
|
||||
out.append(" | ")
|
||||
digit_range(chr(ord(c) + 1), "9")
|
||||
out.append(" ")
|
||||
more_digits(length - 1, less_decimals)
|
||||
return
|
||||
|
||||
if max_value is not None:
|
||||
if max_value >= 0:
|
||||
if top_level:
|
||||
out.append("\"-\" [1-9] ")
|
||||
more_digits(0, less_decimals)
|
||||
out.append(" | ")
|
||||
_generate_min_max_int(0, max_value, out, decimals_left, top_level=True)
|
||||
else:
|
||||
out.append("\"-\" (")
|
||||
_generate_min_max_int(-max_value, None, out, decimals_left, top_level=False)
|
||||
out.append(")")
|
||||
return
|
||||
|
||||
raise RuntimeError("At least one of min_value or max_value must be set")
|
||||
|
||||
class BuiltinRule:
|
||||
def __init__(self, content: str, deps: list | None = None):
|
||||
self.content = content
|
||||
self.deps = deps or []
|
||||
|
||||
# Constraining spaces to prevent model "running away".
|
||||
SPACE_RULE = '| " " | "\\n"{1,2} [ \\t]{0,20}'
|
||||
|
||||
PRIMITIVE_RULES = {
|
||||
'boolean' : BuiltinRule('("true" | "false")', []),
|
||||
'decimal-part' : BuiltinRule('[0-9]{1,16}', []),
|
||||
'integral-part': BuiltinRule('[0] | [1-9] [0-9]{0,15}', []),
|
||||
'number' : BuiltinRule('("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?', ['integral-part', 'decimal-part']),
|
||||
'integer' : BuiltinRule('("-"? integral-part)', ['integral-part']),
|
||||
'value' : BuiltinRule('object | array | string | number | boolean | null', ['object', 'array', 'string', 'number', 'boolean', 'null']),
|
||||
'object' : BuiltinRule('"{" space ( string ":" space value ("," space string ":" space value)* )? space "}"', ['string', 'value']),
|
||||
'array' : BuiltinRule('"[" space ( value ("," space value)* )? space "]"', ['value']),
|
||||
'uuid' : BuiltinRule(r'"\"" [0-9a-fA-F]{8} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{4} "-" [0-9a-fA-F]{12} "\""', []),
|
||||
'char' : BuiltinRule(r'[^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})', []),
|
||||
'string' : BuiltinRule(r'"\"" char* "\""', ['char']),
|
||||
'null' : BuiltinRule('"null"', []),
|
||||
}
|
||||
|
||||
# TODO: support "uri", "email" string formats
|
||||
STRING_FORMAT_RULES = {
|
||||
'date' : BuiltinRule('[0-9]{4} "-" ( "0" [1-9] | "1" [0-2] ) "-" ( \"0\" [1-9] | [1-2] [0-9] | "3" [0-1] )', []),
|
||||
'time' : BuiltinRule('([01] [0-9] | "2" [0-3]) ":" [0-5] [0-9] ":" [0-5] [0-9] ( "." [0-9]{3} )? ( "Z" | ( "+" | "-" ) ( [01] [0-9] | "2" [0-3] ) ":" [0-5] [0-9] )', []),
|
||||
'date-time' : BuiltinRule('date "T" time', ['date', 'time']),
|
||||
'date-string' : BuiltinRule('"\\"" date "\\""', ['date']),
|
||||
'time-string' : BuiltinRule('"\\"" time "\\""', ['time']),
|
||||
'date-time-string': BuiltinRule('"\\"" date-time "\\""', ['date-time']),
|
||||
}
|
||||
|
||||
DOTALL = '[\\U00000000-\\U0010FFFF]'
|
||||
DOT = '[^\\x0A\\x0D]'
|
||||
|
||||
RESERVED_NAMES = set(["root", "dot", *PRIMITIVE_RULES.keys(), *STRING_FORMAT_RULES.keys()])
|
||||
|
||||
INVALID_RULE_CHARS_RE = re.compile(r'[^a-zA-Z0-9-]+')
|
||||
GRAMMAR_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\\]')
|
||||
GRAMMAR_RANGE_LITERAL_ESCAPE_RE = re.compile(r'[\r\n"\]\-\\]')
|
||||
GRAMMAR_LITERAL_ESCAPES = {'\r': '\\r', '\n': '\\n', '"': '\\"', '-': '\\-', ']': '\\]', '\\': '\\\\'}
|
||||
|
||||
NON_LITERAL_SET = set('|.()[]{}*+?')
|
||||
ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = set('^$.[]()|{}*+?')
|
||||
|
||||
|
||||
class SchemaConverter:
|
||||
def __init__(self, *, prop_order, allow_fetch, dotall, raw_pattern):
|
||||
self._prop_order = prop_order
|
||||
self._allow_fetch = allow_fetch
|
||||
self._dotall = dotall
|
||||
self._raw_pattern = raw_pattern
|
||||
self._rules = {
|
||||
'space': SPACE_RULE,
|
||||
}
|
||||
self._refs = {}
|
||||
self._refs_being_resolved = set()
|
||||
|
||||
def _format_literal(self, literal):
|
||||
escaped = GRAMMAR_LITERAL_ESCAPE_RE.sub(
|
||||
lambda m: GRAMMAR_LITERAL_ESCAPES.get(m.group(0)) or m.group(0), literal
|
||||
)
|
||||
return f'"{escaped}"'
|
||||
|
||||
def not_literal(self, literal: str, dotall: bool = True, maybe_escaped_underscores = False) -> str:
|
||||
'''
|
||||
not_literal('a') -> '[^a]'
|
||||
not_literal('abc') -> '([^a] | "a" ([^b] | "b" ([^c])?)?)?'
|
||||
'''
|
||||
assert len(literal) > 0, 'Empty literal not supported'
|
||||
def recurse(i: int):
|
||||
c = literal[i]
|
||||
if maybe_escaped_underscores and c == '_':
|
||||
yield f'[^{c}\\\\]'
|
||||
yield ' | '
|
||||
yield f'"\\\\"? "{c}"'
|
||||
else:
|
||||
yield f'[^{c}]'
|
||||
if i < len(literal) - 1:
|
||||
yield ' | '
|
||||
yield self._format_literal(c)
|
||||
yield ' ('
|
||||
yield from recurse(i + 1)
|
||||
yield ')?'
|
||||
|
||||
return ''.join(('(', *recurse(0), ')'))
|
||||
|
||||
def _not_strings(self, strings):
|
||||
class TrieNode:
|
||||
def __init__(self):
|
||||
self.children = {}
|
||||
self.is_end_of_string = False
|
||||
|
||||
def insert(self, string):
|
||||
node = self
|
||||
for c in string:
|
||||
node = node.children.setdefault(c, TrieNode())
|
||||
node.is_end_of_string = True
|
||||
|
||||
trie = TrieNode()
|
||||
for s in strings:
|
||||
trie.insert(s)
|
||||
|
||||
char_rule = self._add_primitive('char', PRIMITIVE_RULES['char'])
|
||||
out = ['["] ( ']
|
||||
|
||||
def visit(node):
|
||||
rejects = []
|
||||
first = True
|
||||
for c in sorted(node.children.keys()):
|
||||
child = node.children[c]
|
||||
rejects.append(c)
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
out.append(' | ')
|
||||
out.append(f'[{c}]')
|
||||
if child.children:
|
||||
out.append(f' (')
|
||||
visit(child)
|
||||
out.append(')')
|
||||
elif child.is_end_of_string:
|
||||
out.append(f' {char_rule}+')
|
||||
if node.children:
|
||||
if not first:
|
||||
out.append(' | ')
|
||||
out.append(f'[^"{"".join(rejects)}] {char_rule}*')
|
||||
visit(trie)
|
||||
|
||||
out.append(f' ){"" if trie.is_end_of_string else "?"} ["]')
|
||||
return ''.join(out)
|
||||
|
||||
def _add_rule(self, name, rule):
|
||||
esc_name = INVALID_RULE_CHARS_RE.sub('-', name)
|
||||
if esc_name not in self._rules or self._rules[esc_name] == rule:
|
||||
key = esc_name
|
||||
else:
|
||||
i = 0
|
||||
while f'{esc_name}{i}' in self._rules and self._rules[f'{esc_name}{i}'] != rule:
|
||||
i += 1
|
||||
key = f'{esc_name}{i}'
|
||||
self._rules[key] = rule
|
||||
return key
|
||||
|
||||
def resolve_refs(self, schema: dict, url: str):
|
||||
'''
|
||||
Resolves all $ref fields in the given schema, fetching any remote schemas,
|
||||
replacing $ref with absolute reference URL and populating self._refs with the
|
||||
respective referenced (sub)schema dictionaries.
|
||||
'''
|
||||
def visit(n: dict):
|
||||
if isinstance(n, list):
|
||||
return [visit(x) for x in n]
|
||||
elif isinstance(n, dict):
|
||||
ref = n.get('$ref')
|
||||
if ref is not None and ref not in self._refs:
|
||||
if ref.startswith('https://'):
|
||||
assert self._allow_fetch, 'Fetching remote schemas is not allowed (use --allow-fetch for force)'
|
||||
import requests
|
||||
|
||||
frag_split = ref.split('#')
|
||||
base_url = frag_split[0]
|
||||
|
||||
target = self._refs.get(base_url)
|
||||
if target is None:
|
||||
target = self.resolve_refs(requests.get(ref).json(), base_url)
|
||||
self._refs[base_url] = target
|
||||
|
||||
if len(frag_split) == 1 or frag_split[-1] == '':
|
||||
return target
|
||||
elif ref.startswith('#/'):
|
||||
target = schema
|
||||
ref = f'{url}{ref}'
|
||||
n['$ref'] = ref
|
||||
else:
|
||||
raise ValueError(f'Unsupported ref {ref}')
|
||||
|
||||
for sel in ref.split('#')[-1].split('/')[1:]:
|
||||
assert target is not None, f'Error resolving ref {ref}: {sel} not in {target}'
|
||||
if isinstance(target, list):
|
||||
try:
|
||||
sel_index = int(sel)
|
||||
except ValueError:
|
||||
raise ValueError(f'Error resolving ref {ref}: {sel} not in {target}')
|
||||
assert 0 <= sel_index < len(target), f'Error resolving ref {ref}: {sel} not in {target}'
|
||||
target = target[sel_index]
|
||||
else:
|
||||
assert sel in target, f'Error resolving ref {ref}: {sel} not in {target}'
|
||||
target = target[sel]
|
||||
|
||||
self._refs[ref] = target
|
||||
else:
|
||||
for v in n.values():
|
||||
visit(v)
|
||||
|
||||
return n
|
||||
return visit(schema)
|
||||
|
||||
def _generate_union_rule(self, name, alt_schemas):
|
||||
return ' | '.join((
|
||||
self.visit(alt_schema, f'{name}{"-" if name else "alternative-"}{i}')
|
||||
for i, alt_schema in enumerate(alt_schemas)
|
||||
))
|
||||
|
||||
def _visit_pattern(self, pattern, name):
|
||||
'''
|
||||
Transforms a regular expression pattern into a GBNF rule.
|
||||
|
||||
Input: https://json-schema.org/understanding-json-schema/reference/regular_expressions
|
||||
Output: https://github.com/ggml-org/llama.cpp/blob/master/grammars/README.md
|
||||
|
||||
Unsupported features: negative/positive lookaheads, greedy/non-greedy modifiers.
|
||||
|
||||
Mostly a 1:1 translation, except for {x} / {x,} / {x,y} quantifiers for which
|
||||
we define sub-rules to keep the output lean.
|
||||
'''
|
||||
|
||||
assert pattern.startswith('^') and pattern.endswith('$'), 'Pattern must start with "^" and end with "$"'
|
||||
pattern = pattern[1:-1]
|
||||
sub_rule_ids = {}
|
||||
|
||||
i = 0
|
||||
length = len(pattern)
|
||||
|
||||
def to_rule(s: tuple[str, bool]) -> str:
|
||||
(txt, is_literal) = s
|
||||
return "\"" + txt + "\"" if is_literal else txt
|
||||
|
||||
def transform() -> tuple[str, bool]:
|
||||
'''
|
||||
Parse a unit at index i (advancing it), and return its string representation + whether it's a literal.
|
||||
'''
|
||||
nonlocal i
|
||||
nonlocal pattern
|
||||
nonlocal sub_rule_ids
|
||||
|
||||
start = i
|
||||
# For each component of this sequence, store its string representation and whether it's a literal.
|
||||
# We only need a flat structure here to apply repetition operators to the last item, and
|
||||
# to merge literals at the and (we're parsing grouped ( sequences ) recursively and don't treat '|' specially
|
||||
# (GBNF's syntax is luckily very close to regular expressions!)
|
||||
seq: list[tuple[str, bool]] = []
|
||||
|
||||
def get_dot():
|
||||
if self._dotall:
|
||||
rule = DOTALL
|
||||
else:
|
||||
# Accept any character... except \n and \r line break chars (\x0A and \xOD)
|
||||
rule = DOT
|
||||
return self._add_rule(f'dot', rule)
|
||||
|
||||
def join_seq():
|
||||
nonlocal seq
|
||||
ret = []
|
||||
for is_literal, g in itertools.groupby(seq, lambda x: x[1]):
|
||||
if is_literal:
|
||||
ret.append((''.join(x[0] for x in g), True))
|
||||
else:
|
||||
ret.extend(g)
|
||||
if len(ret) == 1:
|
||||
return ret[0]
|
||||
return (' '.join(to_rule(x) for x in seq), False)
|
||||
|
||||
while i < length:
|
||||
c = pattern[i]
|
||||
if c == '.':
|
||||
seq.append((get_dot(), False))
|
||||
i += 1
|
||||
elif c == '(':
|
||||
i += 1
|
||||
if i < length:
|
||||
assert pattern[i] != '?', f'Unsupported pattern syntax "{pattern[i]}" at index {i} of /{pattern}/'
|
||||
seq.append((f'({to_rule(transform())})', False))
|
||||
elif c == ')':
|
||||
i += 1
|
||||
assert start > 0 and pattern[start-1] == '(', f'Unbalanced parentheses; start = {start}, i = {i}, pattern = {pattern}'
|
||||
return join_seq()
|
||||
elif c == '[':
|
||||
square_brackets = c
|
||||
i += 1
|
||||
while i < length and pattern[i] != ']':
|
||||
if pattern[i] == '\\':
|
||||
square_brackets += pattern[i:i+2]
|
||||
i += 2
|
||||
else:
|
||||
square_brackets += pattern[i]
|
||||
i += 1
|
||||
assert i < length, f'Unbalanced square brackets; start = {start}, i = {i}, pattern = {pattern}'
|
||||
square_brackets += ']'
|
||||
i += 1
|
||||
seq.append((square_brackets, False))
|
||||
elif c == '|':
|
||||
seq.append(('|', False))
|
||||
i += 1
|
||||
elif c in ('*', '+', '?'):
|
||||
seq[-1] = (to_rule(seq[-1]) + c, False)
|
||||
i += 1
|
||||
elif c == '{':
|
||||
curly_brackets = c
|
||||
i += 1
|
||||
while i < length and pattern[i] != '}':
|
||||
curly_brackets += pattern[i]
|
||||
i += 1
|
||||
assert i < length, f'Unbalanced curly brackets; start = {start}, i = {i}, pattern = {pattern}'
|
||||
curly_brackets += '}'
|
||||
i += 1
|
||||
nums = [s.strip() for s in curly_brackets[1:-1].split(',')]
|
||||
min_times = 0
|
||||
max_times = None
|
||||
try:
|
||||
if len(nums) == 1:
|
||||
min_times = int(nums[0])
|
||||
max_times = min_times
|
||||
else:
|
||||
assert len(nums) == 2
|
||||
min_times = int(nums[0]) if nums[0] else 0
|
||||
max_times = int(nums[1]) if nums[1] else None
|
||||
except ValueError:
|
||||
raise ValueError(f'Invalid quantifier {curly_brackets} in /{pattern}/')
|
||||
|
||||
(sub, sub_is_literal) = seq[-1]
|
||||
|
||||
if not sub_is_literal:
|
||||
id = sub_rule_ids.get(sub)
|
||||
if id is None:
|
||||
id = self._add_rule(f'{name}-{len(sub_rule_ids) + 1}', sub)
|
||||
sub_rule_ids[sub] = id
|
||||
sub = id
|
||||
|
||||
seq[-1] = (_build_repetition(f'"{sub}"' if sub_is_literal else sub, min_times, max_times), False)
|
||||
else:
|
||||
literal = ''
|
||||
while i < length:
|
||||
if pattern[i] == '\\' and i < length - 1:
|
||||
next = pattern[i + 1]
|
||||
if next in ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS:
|
||||
i += 1
|
||||
literal += pattern[i]
|
||||
i += 1
|
||||
else:
|
||||
literal += pattern[i:i+2]
|
||||
i += 2
|
||||
elif pattern[i] == '"' and not self._raw_pattern:
|
||||
literal += '\\"'
|
||||
i += 1
|
||||
elif pattern[i] not in NON_LITERAL_SET and \
|
||||
(i == length - 1 or literal == '' or pattern[i+1] == '.' or pattern[i+1] not in NON_LITERAL_SET):
|
||||
literal += pattern[i]
|
||||
i += 1
|
||||
else:
|
||||
break
|
||||
if literal:
|
||||
seq.append((literal, True))
|
||||
|
||||
return join_seq()
|
||||
|
||||
return self._add_rule(
|
||||
name,
|
||||
to_rule(transform()) if self._raw_pattern \
|
||||
else "\"\\\"\" (" + to_rule(transform()) + ") \"\\\"\"")
|
||||
|
||||
|
||||
def _resolve_ref(self, ref):
|
||||
ref_fragment = ref.split('#')[-1]
|
||||
ref_name = 'ref' + re.sub(r'[^a-zA-Z0-9-]+', '-', ref_fragment)
|
||||
if ref_name not in self._rules and ref not in self._refs_being_resolved:
|
||||
self._refs_being_resolved.add(ref)
|
||||
resolved = self._refs[ref]
|
||||
ref_name = self.visit(resolved, ref_name)
|
||||
self._refs_being_resolved.remove(ref)
|
||||
return ref_name
|
||||
|
||||
def _generate_constant_rule(self, value):
|
||||
return self._format_literal(json.dumps(value))
|
||||
|
||||
def visit(self, schema, name):
|
||||
schema_type = schema.get('type')
|
||||
schema_format = schema.get('format')
|
||||
rule_name = name + '-' if name in RESERVED_NAMES else name or 'root'
|
||||
|
||||
if (ref := schema.get('$ref')) is not None:
|
||||
return self._add_rule(rule_name, self._resolve_ref(ref))
|
||||
|
||||
elif 'oneOf' in schema or 'anyOf' in schema:
|
||||
return self._add_rule(rule_name, self._generate_union_rule(name, schema.get('oneOf') or schema['anyOf']))
|
||||
|
||||
elif isinstance(schema_type, list):
|
||||
return self._add_rule(rule_name, self._generate_union_rule(name, [{**schema, 'type': t} for t in schema_type]))
|
||||
|
||||
elif 'const' in schema:
|
||||
return self._add_rule(rule_name, self._generate_constant_rule(schema['const']))
|
||||
|
||||
elif 'enum' in schema:
|
||||
rule = '(' + ' | '.join((self._generate_constant_rule(v) for v in schema['enum'])) + ')'
|
||||
return self._add_rule(rule_name, rule)
|
||||
|
||||
elif schema_type in (None, 'object') and \
|
||||
('properties' in schema or \
|
||||
('additionalProperties' in schema and schema['additionalProperties'] is not True)):
|
||||
required = set(schema.get('required', []))
|
||||
properties = list(schema.get('properties', {}).items())
|
||||
return self._add_rule(rule_name, self._build_object_rule(properties, required, name, schema.get('additionalProperties')))
|
||||
|
||||
elif schema_type in (None, 'object', 'string') and 'allOf' in schema:
|
||||
required = set()
|
||||
properties = []
|
||||
enum_sets = []
|
||||
hybrid_name = name
|
||||
def add_component(comp_schema, is_required):
|
||||
if (ref := comp_schema.get('$ref')) is not None:
|
||||
comp_schema = self._refs[ref]
|
||||
|
||||
if 'properties' in comp_schema:
|
||||
for prop_name, prop_schema in comp_schema['properties'].items():
|
||||
properties.append((prop_name, prop_schema))
|
||||
if is_required:
|
||||
required.add(prop_name)
|
||||
|
||||
if 'enum' in comp_schema:
|
||||
enum_sets.append(set(comp_schema['enum']))
|
||||
|
||||
for t in schema['allOf']:
|
||||
if 'anyOf' in t:
|
||||
for tt in t['anyOf']:
|
||||
add_component(tt, is_required=False)
|
||||
else:
|
||||
add_component(t, is_required=True)
|
||||
|
||||
if enum_sets:
|
||||
enum_intersection = enum_sets[0]
|
||||
for s in enum_sets[1:]:
|
||||
enum_intersection &= s
|
||||
|
||||
if enum_intersection:
|
||||
rule = '(' + ' | '.join((self._generate_constant_rule(v) for v in sorted(enum_intersection))) + ')'
|
||||
return self._add_rule(rule_name, rule)
|
||||
|
||||
return self._add_rule(rule_name, self._build_object_rule(properties, required, hybrid_name, additional_properties=None))
|
||||
|
||||
elif schema_type in (None, 'array') and ('items' in schema or 'prefixItems' in schema):
|
||||
items = schema.get('items', schema.get('prefixItems'))
|
||||
if isinstance(items, list):
|
||||
return self._add_rule(
|
||||
rule_name,
|
||||
'"[" space ' +
|
||||
' "," space '.join(
|
||||
self.visit(item, f'{name}{"-" if name else ""}tuple-{i}')
|
||||
for i, item in enumerate(items)) +
|
||||
' space "]"')
|
||||
else:
|
||||
item_rule_name = self.visit(items, f'{name}{"-" if name else ""}item')
|
||||
min_items = schema.get("minItems", 0)
|
||||
max_items = schema.get("maxItems")
|
||||
return self._add_rule(rule_name, '"[" space ' + _build_repetition(item_rule_name, min_items, max_items, separator_rule='"," space') + ' space "]"')
|
||||
|
||||
elif schema_type in (None, 'string') and 'pattern' in schema:
|
||||
return self._visit_pattern(schema['pattern'], rule_name)
|
||||
|
||||
elif schema_type in (None, 'string') and re.match(r'^uuid[1-5]?$', schema_format or ''):
|
||||
return self._add_primitive(
|
||||
'root' if rule_name == 'root' else schema_format,
|
||||
PRIMITIVE_RULES['uuid']
|
||||
)
|
||||
|
||||
elif schema_type in (None, 'string') and f'{schema_format}-string' in STRING_FORMAT_RULES:
|
||||
prim_name = f'{schema_format}-string'
|
||||
return self._add_rule(rule_name, self._add_primitive(prim_name, STRING_FORMAT_RULES[prim_name]))
|
||||
|
||||
elif schema_type == 'string' and ('minLength' in schema or 'maxLength' in schema):
|
||||
char_rule = self._add_primitive('char', PRIMITIVE_RULES['char'])
|
||||
min_len = schema.get('minLength', 0)
|
||||
max_len = schema.get('maxLength')
|
||||
|
||||
return self._add_rule(rule_name, r'"\"" ' + _build_repetition(char_rule, min_len, max_len) + r' "\""')
|
||||
|
||||
elif schema_type in (None, 'integer') and \
|
||||
('minimum' in schema or 'exclusiveMinimum' in schema or 'maximum' in schema or 'exclusiveMaximum' in schema):
|
||||
min_value = None
|
||||
max_value = None
|
||||
if 'minimum' in schema:
|
||||
min_value = schema['minimum']
|
||||
elif 'exclusiveMinimum' in schema:
|
||||
min_value = schema['exclusiveMinimum'] + 1
|
||||
if 'maximum' in schema:
|
||||
max_value = schema['maximum']
|
||||
elif 'exclusiveMaximum' in schema:
|
||||
max_value = schema['exclusiveMaximum'] - 1
|
||||
|
||||
out = ["("]
|
||||
_generate_min_max_int(min_value, max_value, out)
|
||||
out.append(")")
|
||||
return self._add_rule(rule_name, ''.join(out))
|
||||
|
||||
elif (schema_type == 'object') or (len(schema) == 0):
|
||||
return self._add_rule(rule_name, self._add_primitive('object', PRIMITIVE_RULES['object']))
|
||||
|
||||
elif schema_type is None and isinstance(schema, dict):
|
||||
# No type constraint and no recognized structural keywords (e.g. {"description": "..."}).
|
||||
# Per JSON Schema semantics this is equivalent to {} and accepts any value.
|
||||
return self._add_rule(rule_name, self._add_primitive('value', PRIMITIVE_RULES['value']))
|
||||
|
||||
else:
|
||||
assert schema_type in PRIMITIVE_RULES, f'Unrecognized schema: {schema}'
|
||||
# TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero
|
||||
return self._add_primitive('root' if rule_name == 'root' else schema_type, PRIMITIVE_RULES[schema_type])
|
||||
|
||||
def _add_primitive(self, name: str, rule: BuiltinRule):
|
||||
n = self._add_rule(name, rule.content)
|
||||
|
||||
for dep in rule.deps:
|
||||
dep_rule = PRIMITIVE_RULES.get(dep) or STRING_FORMAT_RULES.get(dep)
|
||||
assert dep_rule, f'Rule {dep} not known'
|
||||
if dep not in self._rules:
|
||||
self._add_primitive(dep, dep_rule)
|
||||
return n
|
||||
|
||||
def _build_object_rule(self, properties: List[Tuple[str, Any]], required: Set[str], name: str, additional_properties: Optional[Union[bool, Any]]):
|
||||
prop_order = self._prop_order
|
||||
# sort by position in prop_order (if specified) then by original order
|
||||
sorted_props = [kv[0] for _, kv in sorted(enumerate(properties), key=lambda ikv: (prop_order.get(ikv[1][0], len(prop_order)), ikv[0]))]
|
||||
|
||||
prop_kv_rule_names = {}
|
||||
for prop_name, prop_schema in properties:
|
||||
prop_rule_name = self.visit(prop_schema, f'{name}{"-" if name else ""}{prop_name}')
|
||||
prop_kv_rule_names[prop_name] = self._add_rule(
|
||||
f'{name}{"-" if name else ""}{prop_name}-kv',
|
||||
fr'{self._format_literal(json.dumps(prop_name))} space ":" space {prop_rule_name}'
|
||||
)
|
||||
required_props = [k for k in sorted_props if k in required]
|
||||
optional_props = [k for k in sorted_props if k not in required]
|
||||
|
||||
if additional_properties is not None and additional_properties != False:
|
||||
sub_name = f'{name}{"-" if name else ""}additional'
|
||||
value_rule = self.visit(additional_properties, f'{sub_name}-value') if isinstance(additional_properties, dict) else \
|
||||
self._add_primitive('value', PRIMITIVE_RULES['value'])
|
||||
key_rule = self._add_primitive('string', PRIMITIVE_RULES['string']) if not sorted_props \
|
||||
else self._add_rule(f'{sub_name}-k', self._not_strings(sorted_props))
|
||||
|
||||
prop_kv_rule_names["*"] = self._add_rule(
|
||||
f'{sub_name}-kv',
|
||||
f'{key_rule} ":" space {value_rule}'
|
||||
)
|
||||
optional_props.append("*")
|
||||
|
||||
if not required_props and not optional_props:
|
||||
return '"{" space "}"'
|
||||
|
||||
rule = '"{" space '
|
||||
rule += ' "," space '.join(prop_kv_rule_names[k] for k in required_props)
|
||||
|
||||
if optional_props:
|
||||
rule += ' ('
|
||||
if required_props:
|
||||
rule += ' "," space ( '
|
||||
|
||||
def get_recursive_refs(ks, first_is_optional):
|
||||
[k, *rest] = ks
|
||||
kv_rule_name = prop_kv_rule_names[k]
|
||||
comma_ref = f'( "," space {kv_rule_name} )'
|
||||
if first_is_optional:
|
||||
res = comma_ref + ('*' if k == '*' else '?')
|
||||
else:
|
||||
res = kv_rule_name + (' ' + comma_ref + "*" if k == '*' else '')
|
||||
if len(rest) > 0:
|
||||
res += ' ' + self._add_rule(
|
||||
f'{name}{"-" if name else ""}{k}-rest',
|
||||
get_recursive_refs(rest, first_is_optional=True)
|
||||
)
|
||||
return res
|
||||
|
||||
rule += ' | '.join(
|
||||
get_recursive_refs(optional_props[i:], first_is_optional=False)
|
||||
for i in range(len(optional_props))
|
||||
)
|
||||
if required_props:
|
||||
rule += ' )'
|
||||
rule += ' )?'
|
||||
|
||||
rule += ' space "}"'
|
||||
|
||||
return rule
|
||||
|
||||
def format_grammar(self):
|
||||
return '\n'.join(
|
||||
f'{name} ::= {rule}'
|
||||
for name, rule in sorted(self._rules.items(), key=lambda kv: kv[0])
|
||||
)
|
||||
|
||||
|
||||
def main(args_in = None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='''
|
||||
Generates a grammar (suitable for use in ./llama-cli) that produces JSON conforming to a
|
||||
given JSON schema. Only a subset of JSON schema features are supported; more may be
|
||||
added in the future.
|
||||
''',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--prop-order',
|
||||
default=[],
|
||||
type=lambda s: s.split(','),
|
||||
help='''
|
||||
comma-separated property names defining the order of precedence for object properties;
|
||||
properties not specified here are given lower precedence than those that are, and
|
||||
are kept in their original order from the schema. Required properties are always
|
||||
given precedence over optional properties.
|
||||
'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'--allow-fetch',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Whether to allow fetching referenced schemas over HTTPS')
|
||||
parser.add_argument(
|
||||
'--dotall',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Whether to treat dot (".") as matching all chars including line breaks in regular expression patterns')
|
||||
parser.add_argument(
|
||||
'--raw-pattern',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Treats string patterns as raw patterns w/o quotes (or quote escapes)')
|
||||
|
||||
parser.add_argument('schema', help='file containing JSON schema ("-" for stdin)')
|
||||
args = parser.parse_args(args_in)
|
||||
|
||||
if args.schema.startswith('https://'):
|
||||
url = args.schema
|
||||
import requests
|
||||
schema = requests.get(url).json()
|
||||
elif args.schema == '-':
|
||||
url = 'stdin'
|
||||
schema = json.load(sys.stdin)
|
||||
else:
|
||||
url = f'file://{args.schema}'
|
||||
with open(args.schema) as f:
|
||||
schema = json.load(f)
|
||||
converter = SchemaConverter(
|
||||
prop_order={name: idx for idx, name in enumerate(args.prop_order)},
|
||||
allow_fetch=args.allow_fetch,
|
||||
dotall=args.dotall,
|
||||
raw_pattern=args.raw_pattern)
|
||||
schema = converter.resolve_refs(schema, url)
|
||||
converter.visit(schema, '')
|
||||
print(converter.format_grammar())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,20 +0,0 @@
|
||||
import json, subprocess, sys, os
|
||||
|
||||
assert len(sys.argv) >= 2
|
||||
[_, pattern, *rest] = sys.argv
|
||||
|
||||
print(subprocess.check_output(
|
||||
[
|
||||
"python",
|
||||
os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"json_schema_to_grammar.py"),
|
||||
*rest,
|
||||
"-",
|
||||
"--raw-pattern",
|
||||
],
|
||||
text=True,
|
||||
input=json.dumps({
|
||||
"type": "string",
|
||||
"pattern": pattern,
|
||||
}, indent=2)))
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# ./examples/ts-type-to-grammar.sh "{a:string,b:string,c?:string}"
|
||||
# python examples/json_schema_to_grammar.py https://json.schemastore.org/tsconfig.json
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
readonly type="$1"
|
||||
|
||||
# Create a temporary directory
|
||||
TMPDIR=""
|
||||
trap 'rm -fR "$TMPDIR"' EXIT
|
||||
TMPDIR=$(mktemp -d)
|
||||
|
||||
DTS_FILE="$TMPDIR/type.d.ts"
|
||||
SCHEMA_FILE="$TMPDIR/schema.json"
|
||||
|
||||
echo "export type MyType = $type" > "$DTS_FILE"
|
||||
|
||||
# This is a fork of typescript-json-schema, actively maintained as of March 2024:
|
||||
# https://github.com/vega/ts-json-schema-generator
|
||||
npx ts-json-schema-generator --unstable --no-top-ref --path "$DTS_FILE" --type MyType -e none > "$SCHEMA_FILE"
|
||||
|
||||
# Alternative, not actively maintained as of March 2024:
|
||||
# https://github.com/YousefED/typescript-json-schema
|
||||
# npx typescript-json-schema --defaultProps --required "$DTS_FILE" MyType | tee "$SCHEMA_FILE" >&2
|
||||
|
||||
./examples/json_schema_to_grammar.py "$SCHEMA_FILE"
|
||||
@@ -0,0 +1,281 @@
|
||||
static constexpr __host__ __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config_gcn(ggml_type type, int J, bool fallback) {
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q1_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_0, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_1, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_1, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_1, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_1, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 64, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 80, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 96, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 112, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q8_0, 512, 2, 128, 128, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
CASE(GGML_TYPE_Q2_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q2_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q2_K, 512, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q2_K, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q3_K, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q3_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q3_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q4_K, 256, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 3, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q5_K, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_Q6_K, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q6_K, MMQ_ITER_K, false, false);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ1_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ2_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q3_K, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_XXS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ3_S, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_XS, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_IQ4_NL, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, MMQ_ITER_K, false, false);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_MXFP4, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_MXFP4, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 512, 2, 128, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 64, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_MXFP4, 512, 2, 128, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_1, MMQ_ITER_K, false, false);
|
||||
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, true);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 8, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 16, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 24, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 32, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 128, 40, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 48, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
CASE(GGML_TYPE_NVFP4, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_NVFP4, MMQ_ITER_K, false, false);
|
||||
|
||||
return ggml_cuda_mmq_config(GGML_TYPE_COUNT, 256, 2, 64, 64, GGML_CUDA_MMQ_SRAM_LAYOUT_Q8_0, 256, false, true);
|
||||
}
|
||||
@@ -218,6 +218,7 @@ struct ggml_cuda_mmq_config {
|
||||
#include "mmq-config-ampere.cuh"
|
||||
#include "mmq-config-blackwell.cuh"
|
||||
|
||||
#include "mmq-config-gcn.cuh"
|
||||
#include "mmq-config-cdna.cuh"
|
||||
#include "mmq-config-rdna2.cuh"
|
||||
#include "mmq-config-rdna3.cuh"
|
||||
@@ -228,6 +229,9 @@ struct ggml_cuda_mmq_config {
|
||||
|
||||
static __host__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(const ggml_type type, const int J, const bool fallback, const int cc) {
|
||||
if (GGML_CUDA_CC_IS_AMD(cc)) {
|
||||
if (GGML_CUDA_CC_IS_GCN(cc)) {
|
||||
return ggml_cuda_mmq_get_config_gcn(type, J, fallback);
|
||||
}
|
||||
if (GGML_CUDA_CC_IS_CDNA(cc)) {
|
||||
return ggml_cuda_mmq_get_config_cdna(type, J, fallback);
|
||||
}
|
||||
@@ -256,7 +260,9 @@ static __host__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(const ggml_type ty
|
||||
|
||||
static constexpr __device__ ggml_cuda_mmq_config ggml_cuda_mmq_get_config(ggml_type type, int J, bool fallback) {
|
||||
#ifdef GGML_USE_HIP
|
||||
#ifdef CDNA
|
||||
#ifdef GCN
|
||||
return ggml_cuda_mmq_get_config_gcn(type, J, fallback);
|
||||
#elif defined(CDNA)
|
||||
return ggml_cuda_mmq_get_config_cdna(type, J, fallback);
|
||||
#elif defined(RDNA4)
|
||||
return ggml_cuda_mmq_get_config_rdna4(type, J, fallback);
|
||||
|
||||
@@ -8302,9 +8302,20 @@ inline bool use_adreno_kernels(const ggml_backend_opencl_context *backend_ctx, c
|
||||
bool threashold_ok = tensor->ne[0] >= threshold_ne0 && tensor->ne[1] >= threshold_ne1 &&
|
||||
tensor->ne[2] == 1 && tensor->ne[3] == 1;
|
||||
|
||||
// q6_K adreno kernels requires ne1 is multiple of 128
|
||||
if (tensor->type == GGML_TYPE_Q6_K) {
|
||||
return threashold_ok && tensor->ne[1] % 128 == 0;
|
||||
// The noshuffle layout packs 2 rows per 32-bit texel and the GEMV reads it at an
|
||||
// ne1/2 texel stride with an exact-cover dispatch, so it is only addressable when
|
||||
// ne1 is a multiple of 64; an unaligned ne1 truncates the stride and the weight is
|
||||
// read misaligned. That is a property of the layout, not of one quant -- q4_K, q5_K
|
||||
// and q8_0 read the same packing as q6_K. The bound is 64, not 128: a q8_0 attention
|
||||
// weight of ne1 = 2880 is a multiple of 64 but not 128 and is correct.
|
||||
switch (tensor->type) {
|
||||
case GGML_TYPE_Q4_K:
|
||||
case GGML_TYPE_Q5_K:
|
||||
case GGML_TYPE_Q6_K:
|
||||
case GGML_TYPE_Q8_0:
|
||||
return threashold_ok && tensor->ne[1] % 64 == 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return threashold_ok;
|
||||
}
|
||||
|
||||
@@ -333,6 +333,7 @@ static void ggml_vk_print_device_lost_info(const vk_device& device);
|
||||
struct vk_queue_handle {
|
||||
vk::Queue queue;
|
||||
vk_device_ref device;
|
||||
std::mutex * device_submit_mutex = nullptr;
|
||||
virtual void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) = 0;
|
||||
virtual void lock() {} // no-op by default (internally synchronized case)
|
||||
virtual void unlock() {}
|
||||
@@ -342,6 +343,11 @@ struct vk_queue_handle {
|
||||
struct vk_queue_handle_synchronized : vk_queue_handle {
|
||||
std::mutex mutex;
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
// Workaround for NVIDIA driver bug
|
||||
std::unique_lock<std::mutex> device_guard;
|
||||
if (device_submit_mutex) {
|
||||
device_guard = std::unique_lock<std::mutex>(*device_submit_mutex);
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
try {
|
||||
queue.submit(submits, fence);
|
||||
@@ -356,9 +362,14 @@ struct vk_queue_handle_synchronized : vk_queue_handle {
|
||||
void unlock() override { mutex.unlock(); }
|
||||
};
|
||||
|
||||
// Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues
|
||||
struct vk_queue_handle_unsynchronized : vk_queue_handle {
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
// Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues
|
||||
// Workaround for NVIDIA driver bug
|
||||
std::unique_lock<std::mutex> device_guard;
|
||||
if (device_submit_mutex) {
|
||||
device_guard = std::unique_lock<std::mutex>(*device_submit_mutex);
|
||||
}
|
||||
try {
|
||||
queue.submit(submits, fence);
|
||||
} catch (vk::DeviceLostError &) {
|
||||
@@ -835,6 +846,7 @@ static bool ggml_vk_lightning_indexer_k_type_supported(ggml_type type) {
|
||||
|
||||
struct vk_device_struct {
|
||||
std::recursive_mutex mutex;
|
||||
std::mutex queue_submit_mutex;
|
||||
mutable std::shared_mutex pinned_memory_mutex;
|
||||
|
||||
// Guards compile_pending, all_pipelines, and the dynamic pipeline maps
|
||||
@@ -3520,6 +3532,10 @@ static std::unique_ptr<vk_queue> ggml_vk_create_queue(vk_device& device, uint32_
|
||||
|
||||
h->queue = device->device.getQueue2(queue_info2);
|
||||
h->device = device;
|
||||
// Avoid concurrent submissions on NVIDIA due to driver bug.
|
||||
if (device->vendor_id == VK_VENDOR_ID_NVIDIA) {
|
||||
h->device_submit_mutex = &device->queue_submit_mutex;
|
||||
}
|
||||
q->handle = h;
|
||||
|
||||
q->cmd_pool.init(device, q.get());
|
||||
|
||||
+1
-7
@@ -146,8 +146,6 @@ You can use GBNF grammars:
|
||||
- For any completion endpoints, passed as the `json_schema` body field
|
||||
- For the `/chat/completions` endpoint, passed inside the `response_format` body field (e.g. `{"type", "json_object", "schema": {"items": {}}}` or `{ type: "json_schema", json_schema: {"schema": ...} }`)
|
||||
- In [llama-cli](../tools/cli) and [llama-completion](../tools/completion), passed as the `--json` / `-j` flag
|
||||
- To convert to a grammar ahead of time:
|
||||
- in CLI, with [examples/json_schema_to_grammar.py](../examples/json_schema_to_grammar.py)
|
||||
|
||||
> [!NOTE]
|
||||
> The JSON schema is only used to constrain the model output and is not injected into the prompt. The model has no visibility into the schema, so if you want it to understand the expected structure, describe it explicitly in your prompt. This does not apply to tool calling, where schemas are injected into the prompt.
|
||||
@@ -187,11 +185,7 @@ llama-cli \
|
||||
|
||||
<summary>Show grammar</summary>
|
||||
|
||||
You can convert any schema in command-line with:
|
||||
|
||||
```bash
|
||||
examples/json_schema_to_grammar.py name-age-schema.json
|
||||
```
|
||||
The schema above converts to:
|
||||
|
||||
```
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
HTTPLIB_VERSION = "refs/tags/v0.54.1"
|
||||
HTTPLIB_VERSION = "refs/tags/v0.56.0"
|
||||
|
||||
# used by examples/gguf-hash, these repos have no release tag, so we pin a commit
|
||||
XXHASH_COMMIT = "9f465f1ea932d6ad9a26cd77496311ffa544cd68"
|
||||
|
||||
+104
-53
@@ -21,6 +21,7 @@ set(DIST_DIR "${UI_BINARY_DIR}/dist")
|
||||
set(SRC_DIST_DIR "${UI_SOURCE_DIR}/dist")
|
||||
set(WORK_DIR "${UI_BINARY_DIR}/ui-src")
|
||||
set(STAMP_FILE "${UI_BINARY_DIR}/.ui-stamp")
|
||||
set(EMBED_STAMP "${UI_BINARY_DIR}/.ui-embed.sha256")
|
||||
set(UI_CPP "${UI_BINARY_DIR}/ui.cpp")
|
||||
set(UI_H "${UI_BINARY_DIR}/ui.h")
|
||||
|
||||
@@ -141,9 +142,59 @@ function(ui_validate_assets files in_dir)
|
||||
endfunction()
|
||||
|
||||
# Generate ui.cpp/ui.h embedding every file of ${dist_dir} (empty table when
|
||||
# it has no index.html). When LLAMA_UI_GZIP is enabled, assets are compressed
|
||||
# first and served pre-gzipped (llama_ui_use_gzip()).
|
||||
# it has no index.html), gzip-compressed when LLAMA_UI_GZIP is enabled.
|
||||
function(emit_files dist_dir)
|
||||
set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")
|
||||
|
||||
# Collect the asset list once and reuse it for the fingerprint,
|
||||
# validation, compression and embedding.
|
||||
set(assets "")
|
||||
if(EXISTS "${dist_dir}/index.html")
|
||||
file(GLOB_RECURSE assets
|
||||
LIST_DIRECTORIES false
|
||||
RELATIVE "${dist_dir}"
|
||||
"${dist_dir}/*")
|
||||
list(FILTER assets EXCLUDE REGEX "^_gzip/")
|
||||
list(SORT assets)
|
||||
endif()
|
||||
|
||||
if(LLAMA_UI_GZIP AND NOT DEFINED ENV{SOURCE_DATE_EPOCH})
|
||||
# Zero the gzip header timestamp so identical inputs give identical
|
||||
# bytes (and therefore stable ETags) on every machine.
|
||||
set(ENV{SOURCE_DATE_EPOCH} 0)
|
||||
endif()
|
||||
|
||||
# Fingerprint of every input that determines ui.cpp/ui.h: compression
|
||||
# settings, the asset tree (names + SHA-256) and this script + templates.
|
||||
set(fp "${LLAMA_UI_GZIP}|$ENV{SOURCE_DATE_EPOCH}|${CMAKE_VERSION}\n")
|
||||
foreach(f ${assets})
|
||||
file(SHA256 "${dist_dir}/${f}" h)
|
||||
string(APPEND fp "${f} ${h}\n")
|
||||
endforeach()
|
||||
foreach(g
|
||||
"${CMAKE_CURRENT_FUNCTION_LIST_FILE}"
|
||||
"${UI_TEMPLATE_DIR}/ui.h.in"
|
||||
"${UI_TEMPLATE_DIR}/ui.cpp.in")
|
||||
file(SHA256 "${g}" h)
|
||||
string(APPEND fp "gen ${h}\n")
|
||||
endforeach()
|
||||
string(SHA256 fingerprint "${fp}")
|
||||
|
||||
if(EXISTS "${EMBED_STAMP}" AND EXISTS "${UI_CPP}" AND EXISTS "${UI_H}")
|
||||
file(READ "${EMBED_STAMP}" fp_saved)
|
||||
string(STRIP "${fp_saved}" fp_saved)
|
||||
if(fp_saved STREQUAL "${fingerprint}")
|
||||
message(STATUS "UI: assets unchanged, skipping embedding")
|
||||
return()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Drop the old stamp up front so a crash mid-generation cannot leave
|
||||
# outputs and stamp out of sync.
|
||||
file(REMOVE "${EMBED_STAMP}")
|
||||
|
||||
ui_validate_assets("${assets}" "${dist_dir}")
|
||||
|
||||
set(embed_dir "${dist_dir}")
|
||||
set(use_gzip FALSE)
|
||||
|
||||
@@ -156,21 +207,11 @@ function(emit_files dist_dir)
|
||||
endif()
|
||||
if(LLAMA_UI_GZIP)
|
||||
# Compress every asset into a parallel _gzip/ tree under the build
|
||||
# directory (never write into the source or dist tree); the
|
||||
# structure stays the same: /abc/def --> /_gzip/abc/def.
|
||||
# FORMAT raw produces a bare gzip stream (no archive container)
|
||||
# that can be served with Content-Encoding: gzip. SOURCE_DATE_EPOCH
|
||||
# zeroes the header timestamp so identical inputs give identical
|
||||
# bytes (and therefore stable ETags) on every machine.
|
||||
if(NOT DEFINED ENV{SOURCE_DATE_EPOCH})
|
||||
set(ENV{SOURCE_DATE_EPOCH} 0)
|
||||
endif()
|
||||
# directory, served with Content-Encoding: gzip.
|
||||
set(gzip_root "${UI_BINARY_DIR}/ui-gzip")
|
||||
set(gzip_dir "${gzip_root}/_gzip")
|
||||
file(REMOVE_RECURSE "${gzip_root}")
|
||||
file(GLOB_RECURSE all_files RELATIVE "${dist_dir}" "${dist_dir}/*")
|
||||
list(FILTER all_files EXCLUDE REGEX "^_gzip/")
|
||||
foreach(f ${all_files})
|
||||
foreach(f IN LISTS assets)
|
||||
get_filename_component(asset_path "${dist_dir}/${f}" REALPATH)
|
||||
get_filename_component(dst_dir "${gzip_dir}/${f}" DIRECTORY)
|
||||
file(MAKE_DIRECTORY "${dst_dir}")
|
||||
@@ -187,21 +228,10 @@ function(emit_files dist_dir)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(assets "")
|
||||
if(EXISTS "${embed_dir}/index.html")
|
||||
file(GLOB_RECURSE assets RELATIVE "${embed_dir}" "${embed_dir}/*")
|
||||
list(FILTER assets EXCLUDE REGEX "^_gzip/")
|
||||
list(SORT assets)
|
||||
ui_validate_assets("${assets}" "${embed_dir}")
|
||||
endif()
|
||||
|
||||
list(LENGTH assets n_assets)
|
||||
|
||||
# Only the per-asset data arrays and table rows are built here; all
|
||||
# static C++ lives in the ui.h.in / ui.cpp.in templates. configure_file
|
||||
# rewrites an output only when its contents change, so the library is
|
||||
# not recompiled needlessly. @ONLY keeps ${...} in the content literal;
|
||||
# mime types come from a fixed list.
|
||||
# Per-asset arrays and table rows go into the ui.h.in / ui.cpp.in templates;
|
||||
# configure_file only rewrites on content change, avoiding needless recompiles.
|
||||
set(ASSET_ARRAYS "")
|
||||
set(ASSET_TABLE "")
|
||||
set(idx 0)
|
||||
@@ -235,9 +265,11 @@ function(emit_files dist_dir)
|
||||
set(USE_GZIP true)
|
||||
endif()
|
||||
|
||||
set(UI_TEMPLATE_DIR "${LLAMA_SOURCE_DIR}/tools/ui")
|
||||
configure_file("${UI_TEMPLATE_DIR}/ui.h.in" "${UI_H}" @ONLY)
|
||||
configure_file("${UI_TEMPLATE_DIR}/ui.cpp.in" "${UI_CPP}" @ONLY)
|
||||
|
||||
# Write the embed stamp last, after both generated files succeeded.
|
||||
file(WRITE "${EMBED_STAMP}" "${fingerprint}")
|
||||
message(STATUS "UI: embedded ${n_assets} assets")
|
||||
endfunction()
|
||||
|
||||
@@ -419,16 +451,8 @@ function(hf_download version out_var out_resolved)
|
||||
|
||||
message(STATUS "UI: downloading from ${resolved}: ${base}/dist.tar.gz")
|
||||
|
||||
file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"
|
||||
STATUS status TIMEOUT 300 ${auth_headers}
|
||||
)
|
||||
list(GET status 0 rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
list(GET status 1 errmsg)
|
||||
message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
# Fetch the checksum first: when the archive we already have matches
|
||||
# it, the expensive download is skipped and only extraction repeats.
|
||||
file(DOWNLOAD "${base}/dist.tar.gz.sha256?download=true" "${archive}.sha256"
|
||||
STATUS status TIMEOUT 30 ${auth_headers}
|
||||
)
|
||||
@@ -439,17 +463,44 @@ function(hf_download version out_var out_resolved)
|
||||
continue()
|
||||
endif()
|
||||
|
||||
# Validate sha256 checkums
|
||||
# Validate the sha256 checksum: reject anything that is not a full
|
||||
# 64-hex-digit digest before touching the archive.
|
||||
file(READ "${archive}.sha256" expected)
|
||||
string(REGEX MATCH "^[0-9a-fA-F]+" expected "${expected}")
|
||||
string(TOLOWER "${expected}" expected)
|
||||
file(SHA256 "${archive}" actual)
|
||||
if("${expected}" STREQUAL "" OR NOT "${actual}" STREQUAL "${expected}")
|
||||
message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")
|
||||
string(LENGTH "${expected}" expected_len)
|
||||
if(NOT expected_len EQUAL 64)
|
||||
message(STATUS "UI: invalid checksum from ${resolved}")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
# Clear DIST_DIR to remove stale files first
|
||||
set(actual "")
|
||||
if(EXISTS "${archive}")
|
||||
file(SHA256 "${archive}" actual)
|
||||
endif()
|
||||
|
||||
if("${actual}" STREQUAL "${expected}")
|
||||
message(STATUS "UI: local dist.tar.gz matches checksum from ${resolved}, skipping download")
|
||||
else()
|
||||
file(DOWNLOAD "${base}/dist.tar.gz?download=true" "${archive}"
|
||||
STATUS status TIMEOUT 300 ${auth_headers}
|
||||
)
|
||||
list(GET status 0 rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
list(GET status 1 errmsg)
|
||||
message(STATUS "UI: download dist.tar.gz from ${resolved} failed: ${errmsg}")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
file(SHA256 "${archive}" actual)
|
||||
if(NOT "${actual}" STREQUAL "${expected}")
|
||||
message(STATUS "UI: checksum mismatch for dist.tar.gz from ${resolved}")
|
||||
continue()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Remove the stamp with the dist tree it describes, together.
|
||||
file(REMOVE "${STAMP_FILE}")
|
||||
file(REMOVE_RECURSE "${DIST_DIR}")
|
||||
|
||||
file(ARCHIVE_EXTRACT INPUT "${archive}" DESTINATION "${DIST_DIR}")
|
||||
@@ -495,27 +546,27 @@ endif()
|
||||
if(NOT provisioned AND HF_ENABLED)
|
||||
resolve_version(VERSION)
|
||||
|
||||
# Stamp a successful HF download: records bucket + requested version and
|
||||
# lets later steps distinguish downloaded assets from locally built ones.
|
||||
set(stamp_key "${HF_BUCKET}|${VERSION}")
|
||||
|
||||
set(stamp_ok FALSE)
|
||||
if(EXISTS "${STAMP_FILE}" AND NOT "${VERSION}" STREQUAL "")
|
||||
if(EXISTS "${STAMP_FILE}" AND EXISTS "${DIST_DIR}/index.html" AND NOT "${VERSION}" STREQUAL "")
|
||||
file(READ "${STAMP_FILE}" stamped)
|
||||
string(STRIP "${stamped}" stamped)
|
||||
if("${stamped}" STREQUAL "${VERSION}")
|
||||
if(stamped STREQUAL "${stamp_key}")
|
||||
set(stamp_ok TRUE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(have_assets FALSE)
|
||||
if(EXISTS "${DIST_DIR}/index.html")
|
||||
set(have_assets TRUE)
|
||||
endif()
|
||||
if(stamp_ok AND have_assets)
|
||||
message(STATUS "UI: HF stamp '${stamped}' matches version, skipping HF fetch")
|
||||
if(stamp_ok)
|
||||
message(STATUS "UI: HF stamp matches '${stamp_key}', skipping HF fetch")
|
||||
set(provisioned TRUE)
|
||||
else()
|
||||
hf_download("${VERSION}" HF_OK HF_RESOLVED)
|
||||
if(HF_OK)
|
||||
file(WRITE "${STAMP_FILE}" "${HF_RESOLVED}")
|
||||
message(STATUS "UI: HF download succeeded, stamp updated (${HF_RESOLVED})")
|
||||
file(WRITE "${STAMP_FILE}" "${stamp_key}")
|
||||
message(STATUS "UI: HF download succeeded, stamp updated (${stamp_key}, resolved: ${HF_RESOLVED})")
|
||||
set(provisioned TRUE)
|
||||
else()
|
||||
message(STATUS "UI: HF download failed")
|
||||
|
||||
@@ -163,11 +163,7 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
|
||||
llama_build_and_test(test-chat.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||
target_include_directories(test-chat PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
|
||||
target_link_libraries(test-chat PRIVATE server-context)
|
||||
# TODO: disabled on loongarch64 because the ggml-ci node lacks Python 3.8
|
||||
if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "loongarch64")
|
||||
llama_build_and_test(test-json-schema-to-grammar.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||
target_include_directories(test-json-schema-to-grammar PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
|
||||
endif()
|
||||
llama_build_and_test(test-json-schema-to-grammar.cpp)
|
||||
|
||||
if (NOT GGML_BACKEND_DL)
|
||||
llama_build(test-quantize-stats.cpp)
|
||||
@@ -262,6 +258,7 @@ endif()
|
||||
llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp)
|
||||
llama_build_and_test(test-jinja.cpp)
|
||||
llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python)
|
||||
llama_build_and_test(test-json-schema.cpp)
|
||||
llama_build_and_test(test-chat-auto-parser.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||
llama_build_and_test(test-chat-template.cpp)
|
||||
# debug tool for chat template differential analysis (not registered as a test, run it manually)
|
||||
|
||||
@@ -10678,9 +10678,9 @@ static std::vector<std::unique_ptr<test_case>> make_test_cases_eval() {
|
||||
test_cases.emplace_back(new test_flash_attn_ext(256, 256, 2, {16, 1}, 16384, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0));
|
||||
|
||||
// MLA shape: the V cache is a sub-view of the K cache, with quantized KV
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 113, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {20, 1}, 1024, 64, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 113, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 1024, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
test_cases.emplace_back(new test_flash_attn_ext(576, 512, 1, {8, 1}, 1024, 64, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_Q8_0, GGML_TYPE_Q8_0, {0, 1, 2, 3}, true, true));
|
||||
|
||||
// Sparse mask hint: supported decode/prefill layouts and dense fallbacks.
|
||||
test_cases.emplace_back(new test_flash_attn_ext(512, 512, 1, { 8, 1}, 4096, 1, true, false, 0, 0, GGML_PREC_F32, GGML_TYPE_F16, GGML_TYPE_F16, {0, 1, 2, 3}, true, false, 512));
|
||||
|
||||
@@ -358,11 +358,6 @@ static void test_example_native(testing & t) {
|
||||
auto parser = build_parser(tc);
|
||||
auto lazy = !tc.tools.empty() && tc.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
auto grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
for (const auto & def : tc.tools) {
|
||||
auto function = def.at("function");
|
||||
auto parameters = function.at("parameters");
|
||||
builder.resolve_refs(parameters);
|
||||
};
|
||||
parser.build_grammar(builder, lazy);
|
||||
});
|
||||
|
||||
@@ -440,11 +435,6 @@ static void test_example_qwen3_coder(testing & t) {
|
||||
});
|
||||
|
||||
auto grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
for (const auto & def : tools) {
|
||||
auto function = def.at("function");
|
||||
auto parameters = function.at("parameters");
|
||||
builder.resolve_refs(parameters);
|
||||
};
|
||||
parser.build_grammar(builder);
|
||||
});
|
||||
|
||||
@@ -513,11 +503,6 @@ static void test_example_qwen3_non_coder(testing & t) {
|
||||
});
|
||||
|
||||
auto grammar = build_grammar([&](const common_grammar_builder & builder) {
|
||||
for (const auto & def : tools) {
|
||||
auto function = def.at("function");
|
||||
auto parameters = function.at("parameters");
|
||||
builder.resolve_refs(parameters);
|
||||
};
|
||||
parser.build_grammar(builder);
|
||||
});
|
||||
|
||||
|
||||
@@ -472,6 +472,12 @@ static common_chat_tool empty_args_tool_no_properties{
|
||||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool empty_args_tool_no_schema{
|
||||
/* .name = */ "empty_args_no_schema",
|
||||
/* .description = */ "A tool that takes no arguments and has no parameters schema",
|
||||
/* .parameters = */ "{}",
|
||||
};
|
||||
|
||||
static common_chat_tool python_tool{
|
||||
/* .name = */ "python",
|
||||
/* .description = */ "an ipython interpreter",
|
||||
@@ -840,6 +846,25 @@ static common_chat_tool nullable_int_tool{
|
||||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool string_union_tool{
|
||||
/* .name = */ "set_union",
|
||||
/* .description = */ "Set values whose types are unions with string",
|
||||
/* .parameters = */ R"({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": ["string", "object"],
|
||||
"description": "A string or object value"
|
||||
},
|
||||
"amount": {
|
||||
"type": ["string", "integer"],
|
||||
"description": "A string or integer value"
|
||||
}
|
||||
},
|
||||
"required": ["value", "amount"]
|
||||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool enum_no_type_tool{
|
||||
/* .name = */ "set_unit",
|
||||
/* .description = */ "Set a temperature unit",
|
||||
@@ -3799,6 +3824,46 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
||||
})
|
||||
.run();
|
||||
|
||||
// nullable string given null - parses as JSON null, not the string "null"
|
||||
tst.test(
|
||||
"<tool_call>\n"
|
||||
"<function=set_nullable_str>\n"
|
||||
"<parameter=name>\nnull\n</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>")
|
||||
.tools({ nullable_string_tool })
|
||||
.expect_tool_calls({
|
||||
{ "set_nullable_str", R"({"name": null})", {} },
|
||||
})
|
||||
.run();
|
||||
|
||||
// unions with string - JSON values of the other types are typed, everything else is a string
|
||||
tst.test(
|
||||
"<tool_call>\n"
|
||||
"<function=set_union>\n"
|
||||
"<parameter=value>\n{\"a\": 1}\n</parameter>\n"
|
||||
"<parameter=amount>\n2 dollars\n</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>")
|
||||
.tools({ string_union_tool })
|
||||
.expect_tool_calls({
|
||||
{ "set_union", R"({"value": {"a": 1}, "amount": "2 dollars"})", {} },
|
||||
})
|
||||
.run();
|
||||
|
||||
tst.test(
|
||||
"<tool_call>\n"
|
||||
"<function=set_union>\n"
|
||||
"<parameter=value>\n{not valid json\n</parameter>\n"
|
||||
"<parameter=amount>\n42\n</parameter>\n"
|
||||
"</function>\n"
|
||||
"</tool_call>")
|
||||
.tools({ string_union_tool })
|
||||
.expect_tool_calls({
|
||||
{ "set_union", R"({"value": "{not valid json", "amount": 42})", {} },
|
||||
})
|
||||
.run();
|
||||
|
||||
// enum without explicit type key - should infer string from enum values
|
||||
tst.test(
|
||||
"<tool_call>\n"
|
||||
@@ -5071,6 +5136,13 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
||||
.expect(simple_assist_msg("", "", "empty_args", "{}"))
|
||||
.run();
|
||||
|
||||
// Tool call with no parameters schema, {} means no arguments
|
||||
tst.test("<tool_call>\n{\"name\": \"empty_args_no_schema\", \"arguments\": {}}</tool_call>")
|
||||
.enable_thinking(false)
|
||||
.tools({ empty_args_tool_no_schema })
|
||||
.expect(simple_assist_msg("", "", "empty_args_no_schema", "{}"))
|
||||
.run();
|
||||
|
||||
// fake tool call marker in reasoning
|
||||
tst.test(
|
||||
"Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm\n</think>\n\n"
|
||||
|
||||
@@ -918,7 +918,7 @@ static void test_json_schema() {
|
||||
// Otherwise, this test structure is the same.
|
||||
|
||||
test_schema(
|
||||
"empty schema (object)",
|
||||
"empty schema (any value)",
|
||||
// Schema
|
||||
R"""(
|
||||
{}
|
||||
@@ -927,14 +927,16 @@ static void test_json_schema() {
|
||||
{
|
||||
R"""({})""",
|
||||
R"""({"foo": "bar"})""",
|
||||
},
|
||||
// Failing strings
|
||||
{
|
||||
"",
|
||||
"[]",
|
||||
"null",
|
||||
R"""("")""",
|
||||
"true",
|
||||
},
|
||||
// Failing strings
|
||||
{
|
||||
"",
|
||||
R"""({"foo"})""",
|
||||
"foo",
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -398,6 +398,18 @@ static void test_expressions(testing & t) {
|
||||
"Bob"
|
||||
);
|
||||
|
||||
test_template(t, "dot notation (integer property)",
|
||||
"{{ {10: 'Bob'}.10 }}",
|
||||
json::object(),
|
||||
"Bob"
|
||||
);
|
||||
|
||||
test_template(t, "dot notation (array index)",
|
||||
"{{ user.10 }}",
|
||||
{{"user", json::array({"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"})}},
|
||||
"k"
|
||||
);
|
||||
|
||||
test_template(t, "negative float (not dot notation)",
|
||||
"{{ -1.0 }}",
|
||||
json::object(),
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include "json.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
|
||||
static std::string trim(const std::string & source) {
|
||||
@@ -64,21 +62,8 @@ struct TestCase {
|
||||
}
|
||||
};
|
||||
|
||||
static void write(const std::string & file, const std::string & content) {
|
||||
std::ofstream f;
|
||||
f.open(file.c_str());
|
||||
f << content.c_str();
|
||||
f.close();
|
||||
}
|
||||
|
||||
static std::string read(const std::string & file) {
|
||||
std::ostringstream actuals;
|
||||
actuals << std::ifstream(file.c_str()).rdbuf();
|
||||
return actuals.str();
|
||||
}
|
||||
|
||||
static void test_all(const std::string & lang, std::function<void(const TestCase &)> runner) {
|
||||
fprintf(stderr, "#\n# Testing JSON schema conversion (%s)\n#\n", lang.c_str());
|
||||
static void test_all(const std::string & title, std::function<void(const TestCase &)> runner) {
|
||||
fprintf(stderr, "#\n# %s\n#\n", title.c_str());
|
||||
auto test = [&](const TestCase & tc) {
|
||||
fprintf(stderr, "- %s%s\n", tc.name.c_str(), tc.expected_status == FAILURE ? " (failure expected)" : "");
|
||||
runner(tc);
|
||||
@@ -330,7 +315,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
|
||||
test({
|
||||
SUCCESS,
|
||||
"empty schema (object)",
|
||||
"empty schema (any value)",
|
||||
"{}",
|
||||
R"""(
|
||||
array ::= "[" space ( value ("," space value)* )? space "]"
|
||||
@@ -341,7 +326,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
null ::= "null"
|
||||
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?
|
||||
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}"
|
||||
root ::= object
|
||||
root ::= value
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
value ::= object | array | string | number | boolean | null
|
||||
@@ -569,6 +554,7 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
)"""
|
||||
});
|
||||
|
||||
// items {} constrains nothing, the same as no items at all
|
||||
test({
|
||||
SUCCESS,
|
||||
"array with empty items",
|
||||
@@ -582,11 +568,10 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
decimal-part ::= [0-9]{1,16}
|
||||
integral-part ::= [0] | [1-9] [0-9]{0,15}
|
||||
item ::= object
|
||||
null ::= "null"
|
||||
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?
|
||||
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}"
|
||||
root ::= "[" space (item ("," space item)*)? space "]"
|
||||
root ::= "[" space ( value ("," space value)* )? space "]"
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
value ::= object | array | string | number | boolean | null
|
||||
@@ -607,11 +592,10 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
decimal-part ::= [0-9]{1,16}
|
||||
integral-part ::= [0] | [1-9] [0-9]{0,15}
|
||||
item ::= object
|
||||
null ::= "null"
|
||||
number ::= ("-"? integral-part) ("." decimal-part)? ([eE] [-+]? integral-part)?
|
||||
object ::= "{" space ( string ":" space value ("," space string ":" space value)* )? space "}"
|
||||
root ::= "[" space (item ("," space item)*)? space "]"
|
||||
root ::= "[" space ( value ("," space value)* )? space "]"
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
value ::= object | array | string | number | boolean | null
|
||||
@@ -1434,88 +1418,100 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
)"""
|
||||
});
|
||||
}
|
||||
|
||||
static void test_resolves_to_string() {
|
||||
fprintf(stderr, "#\n# Testing resolves_to_string\n#\n");
|
||||
test({
|
||||
SUCCESS,
|
||||
"regexp with non-capturing group",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(?:foo|bar)baz$"
|
||||
})""",
|
||||
R"""(
|
||||
root ::= "\"" (("foo" | "bar") "baz") "\""
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
)"""
|
||||
});
|
||||
|
||||
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 = common_json::parse(schema_str);
|
||||
info.resolve_refs(schema);
|
||||
bool result = info.resolves_to_string(schema);
|
||||
if (result != expected) {
|
||||
fprintf(stderr, "#\n# Test '%s' failed.\n#\n", name.c_str());
|
||||
fprintf(stderr, "Schema: %s\n", schema_str.c_str());
|
||||
fprintf(stderr, "Expected: %s, Got: %s\n", expected ? "true" : "false", result ? "true" : "false");
|
||||
assert(false);
|
||||
}
|
||||
};
|
||||
test({
|
||||
SUCCESS,
|
||||
"regexp with nested non-capturing groups",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(?:(?:ab)+c)?d$"
|
||||
})""",
|
||||
R"""(
|
||||
root ::= "\"" ((("ab")+ "c")? "d") "\""
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
)"""
|
||||
});
|
||||
|
||||
// Basic type checks
|
||||
test("type string", R"({"type": "string"})", true);
|
||||
test("type integer", R"({"type": "integer"})", false);
|
||||
test("type number", R"({"type": "number"})", false);
|
||||
test("type boolean", R"({"type": "boolean"})", false);
|
||||
test("type object", R"({"type": "object"})", false);
|
||||
test("type array", R"({"type": "array"})", false);
|
||||
test({
|
||||
SUCCESS,
|
||||
"unanchored regexp",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "[0-9]+"
|
||||
})""",
|
||||
R"""(
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= string
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)"""
|
||||
});
|
||||
|
||||
// Type array (nullable string)
|
||||
test("type array with string", R"({"type": ["string", "null"]})", true);
|
||||
test("type array without string", R"({"type": ["integer", "null"]})", false);
|
||||
// the rules of the partial conversion (here "root-0") must not leak into the grammar
|
||||
test({
|
||||
SUCCESS,
|
||||
"regexp with unsupported shorthand",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{3}\\w$"
|
||||
})""",
|
||||
R"""(
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= string
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)"""
|
||||
});
|
||||
|
||||
// String-specific keywords
|
||||
test("minLength implies string", R"({"minLength": 1})", true);
|
||||
test("maxLength implies string", R"({"maxLength": 10})", true);
|
||||
test("pattern implies string", R"({"pattern": "^[a-z]+$"})", true);
|
||||
// a regexp that is invalid under any flavor is still an error
|
||||
test({
|
||||
FAILURE,
|
||||
"regexp with unbalanced parentheses",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(a$"
|
||||
})""",
|
||||
""
|
||||
});
|
||||
|
||||
// Format
|
||||
test("format date", R"({"format": "date"})", true);
|
||||
test("format uuid", R"({"format": "uuid"})", true);
|
||||
test("format email", R"({"format": "email"})", true);
|
||||
|
||||
// Const
|
||||
test("const string", R"({"const": "hello"})", true);
|
||||
test("const number", R"({"const": 123})", false);
|
||||
|
||||
// Enum
|
||||
test("enum with strings", R"({"enum": ["a", "b", "c"]})", true);
|
||||
test("enum with numbers", R"({"enum": [1, 2, 3]})", false);
|
||||
test("enum mixed with string", R"({"enum": [1, "a", null]})", true);
|
||||
|
||||
// anyOf
|
||||
test("anyOf with string", R"({"anyOf": [{"type": "string"}, {"type": "integer"}]})", true);
|
||||
test("anyOf without string", R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false);
|
||||
|
||||
// oneOf
|
||||
test("oneOf with string", R"({"oneOf": [{"type": "string"}, {"type": "number"}]})", true);
|
||||
test("oneOf without string", R"({"oneOf": [{"type": "object"}, {"type": "array"}]})", false);
|
||||
|
||||
// allOf - all must be strings
|
||||
test("allOf all strings", R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true);
|
||||
test("allOf mixed types", R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false);
|
||||
|
||||
// $ref
|
||||
test("$ref to string",
|
||||
R"({"$ref": "#/$defs/str", "$defs": {"str": {"type": "string"}}})", true);
|
||||
test("$ref to integer",
|
||||
R"({"$ref": "#/$defs/num", "$defs": {"num": {"type": "integer"}}})", false);
|
||||
|
||||
// Nested
|
||||
test("nested anyOf with string",
|
||||
R"({"anyOf": [{"anyOf": [{"type": "integer"}, {"type": "string"}]}, {"type": "boolean"}]})", true);
|
||||
|
||||
fprintf(stderr, "All resolves_to_string tests passed!\n");
|
||||
// only the property with the bad pattern degrades
|
||||
test({
|
||||
SUCCESS,
|
||||
"unsupported regexp in a property",
|
||||
R"""({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": { "type": "string", "pattern": "^[a-z\\-]+$" }
|
||||
},
|
||||
"required": ["a"],
|
||||
"additionalProperties": false
|
||||
})""",
|
||||
R"""(
|
||||
a ::= string
|
||||
a-kv ::= "\"a\"" space ":" space a
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= "{" space a-kv space "}"
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)"""
|
||||
});
|
||||
}
|
||||
|
||||
int main() {
|
||||
fprintf(stderr, "LLAMA_NODE_AVAILABLE = %s\n", getenv("LLAMA_NODE_AVAILABLE") ? "true" : "false");
|
||||
fprintf(stderr, "LLAMA_PYTHON_AVAILABLE = %s\n", getenv("LLAMA_PYTHON_AVAILABLE") ? "true" : "false");
|
||||
|
||||
test_resolves_to_string();
|
||||
|
||||
test_all("C++", [](const TestCase & tc) {
|
||||
test_all("JSON schema conversion", [](const TestCase & tc) {
|
||||
try {
|
||||
tc.verify(json_schema_to_grammar(common_json::parse(tc.schema), true));
|
||||
tc.verify_status(SUCCESS);
|
||||
@@ -1525,127 +1521,58 @@ int main() {
|
||||
}
|
||||
});
|
||||
|
||||
// C++ only tests (features not yet supported in JS/Python implementations)
|
||||
// a document parsed up front gives the same grammar as the JSON, recursion included
|
||||
{
|
||||
fprintf(stderr, "#\n# Testing C++ only features\n#\n");
|
||||
auto run = [](const TestCase & tc) {
|
||||
fprintf(stderr, "- %s\n", tc.name.c_str());
|
||||
try {
|
||||
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());
|
||||
tc.verify_status(FAILURE);
|
||||
fprintf(stderr, "- parsed document\n");
|
||||
auto schema = common_json::parse(R"""({
|
||||
"$ref": "#/$defs/node",
|
||||
"$defs": {
|
||||
"node": {
|
||||
"type": "object",
|
||||
"properties": {"next": {"$ref": "#/$defs/node"}, "leaf": {}},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
})""");
|
||||
assert(json_schema_to_grammar(common_chat_schema_from_json(schema)) == json_schema_to_grammar(schema, true));
|
||||
}
|
||||
|
||||
// a property node carries its $ref target, so its grammar names the ref rule
|
||||
{
|
||||
fprintf(stderr, "- sub-schema $ref\n");
|
||||
auto parameters = common_json::parse(R"""({
|
||||
"type": "object",
|
||||
"properties": {"item": {"$ref": "#/$defs/item"}},
|
||||
"$defs": {
|
||||
"item": {
|
||||
"type": "object",
|
||||
"properties": {"a": {"type": "string"}},
|
||||
"required": ["a"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
})""");
|
||||
TestCase tc {
|
||||
SUCCESS,
|
||||
"sub-schema $ref",
|
||||
"",
|
||||
R"""(
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
ref-defs-item ::= "{" space ref-defs-item-a-kv space "}"
|
||||
ref-defs-item-a-kv ::= "\"a\"" space ":" space string
|
||||
root ::= ref-defs-item
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)""",
|
||||
};
|
||||
|
||||
run({
|
||||
SUCCESS,
|
||||
"regexp with non-capturing group",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(?:foo|bar)baz$"
|
||||
})""",
|
||||
R"""(
|
||||
root ::= "\"" (("foo" | "bar") "baz") "\""
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
)""",
|
||||
});
|
||||
|
||||
run({
|
||||
SUCCESS,
|
||||
"regexp with nested non-capturing groups",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(?:(?:ab)+c)?d$"
|
||||
})""",
|
||||
R"""(
|
||||
root ::= "\"" ((("ab")+ "c")? "d") "\""
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
)""",
|
||||
});
|
||||
|
||||
run({
|
||||
SUCCESS,
|
||||
"unanchored regexp",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "[0-9]+"
|
||||
})""",
|
||||
R"""(
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= string
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)""",
|
||||
});
|
||||
|
||||
// the rules of the partial conversion (here "root-0") must not leak into the grammar
|
||||
run({
|
||||
SUCCESS,
|
||||
"regexp with unsupported shorthand",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{3}\\w$"
|
||||
})""",
|
||||
R"""(
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= string
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)""",
|
||||
});
|
||||
|
||||
// a regexp that is invalid under any flavor is still an error
|
||||
run({
|
||||
FAILURE,
|
||||
"regexp with unbalanced parentheses",
|
||||
R"""({
|
||||
"type": "string",
|
||||
"pattern": "^(a$"
|
||||
})""",
|
||||
""
|
||||
});
|
||||
|
||||
// only the property with the bad pattern degrades
|
||||
run({
|
||||
SUCCESS,
|
||||
"unsupported regexp in a property",
|
||||
R"""({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": { "type": "string", "pattern": "^[a-z\\-]+$" }
|
||||
},
|
||||
"required": ["a"],
|
||||
"additionalProperties": false
|
||||
})""",
|
||||
R"""(
|
||||
a ::= string
|
||||
a-kv ::= "\"a\"" space ":" space a
|
||||
char ::= [^"\\\x7F\x00-\x1F] | [\\] (["\\bfnrt] | "u" [0-9a-fA-F]{4})
|
||||
root ::= "{" space a-kv space "}"
|
||||
space ::= | " " | "\n"{1,2} [ \t]{0,20}
|
||||
string ::= "\"" char* "\""
|
||||
)""",
|
||||
});
|
||||
auto doc = common_chat_schema_from_json(parameters);
|
||||
tc.verify(build_grammar([&](const common_grammar_builder & builder) {
|
||||
const auto & item = static_cast<const common_chat_schema_object &>(*doc.root).properties.at(0);
|
||||
builder.add_schema("root", *item.schema);
|
||||
}));
|
||||
}
|
||||
|
||||
if (getenv("LLAMA_SKIP_TESTS_SLOW_ON_EMULATOR")) {
|
||||
fprintf(stderr, "\033[33mWARNING: Skipping slow tests on emulator.\n\033[0m");
|
||||
} else {
|
||||
if (getenv("LLAMA_PYTHON_AVAILABLE") || (std::system("python -c \"import sys; exit(1) if sys.version_info < (3, 8) else print('Python version is sufficient')\"") == 0)) {
|
||||
test_all("Python", [](const TestCase & tc) {
|
||||
write("test-json-schema-input.tmp", tc.schema);
|
||||
tc.verify_status(std::system(
|
||||
"python ./examples/json_schema_to_grammar.py test-json-schema-input.tmp > test-grammar-output.tmp") == 0 ? SUCCESS : FAILURE);
|
||||
tc.verify(read("test-grammar-output.tmp"));
|
||||
});
|
||||
} else {
|
||||
fprintf(stderr, "\033[33mWARNING: Python not found (min version required is 3.8), skipping Python JSON schema -> grammar tests.\n\033[0m");
|
||||
}
|
||||
}
|
||||
|
||||
test_all("Check Expectations Validity", [](const TestCase & tc) {
|
||||
test_all("Check the expectations parse", [](const TestCase & tc) {
|
||||
if (tc.expected_status == SUCCESS) {
|
||||
tc.verify_expectation_parseable();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
#include "json-schema.h"
|
||||
#include "json.h"
|
||||
#include "testing.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
static common_chat_schema_document parse(const std::string & schema) {
|
||||
return common_chat_schema_from_json(common_json::parse(schema));
|
||||
}
|
||||
|
||||
// the node as T, aborting the current test when it is some other kind
|
||||
template <typename T>
|
||||
static const T & as(testing & t, const common_chat_schema * node, const char * what) {
|
||||
const T * typed = dynamic_cast<const T *>(node);
|
||||
if (!t.assert_true(std::string(what) + " has the expected kind", typed != nullptr)) {
|
||||
throw std::runtime_error(std::string(what) + " has the wrong kind");
|
||||
}
|
||||
return *typed;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static const T & root(testing & t, const common_chat_schema_document & doc) {
|
||||
return as<T>(t, doc.root.get(), "root");
|
||||
}
|
||||
|
||||
static void assert_error(testing & t, const std::string & schema, const std::string & needle) {
|
||||
try {
|
||||
parse(schema);
|
||||
t.assert_true(schema + " is rejected", false);
|
||||
} catch (const std::runtime_error & e) {
|
||||
std::string what = e.what();
|
||||
t.assert_true(schema + " -> " + what, what.find(needle) != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_any(testing & t) {
|
||||
t.test("empty schema", [](testing & t) {
|
||||
auto doc = parse("{}");
|
||||
root<common_chat_schema_any>(t, doc);
|
||||
t.assert_true("no refs", doc.refs.empty());
|
||||
});
|
||||
|
||||
t.test("keywords that do not imply a type", [](testing & t) {
|
||||
auto doc = parse(R"({"description": "x", "format": "email", "additionalProperties": true})");
|
||||
root<common_chat_schema_any>(t, doc);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_primitives(testing & t) {
|
||||
t.test("null, boolean, number", [](testing & t) {
|
||||
auto doc_null = parse(R"({"type": "null"})");
|
||||
root<common_chat_schema_null>(t, doc_null);
|
||||
auto doc_bool = parse(R"({"type": "boolean"})");
|
||||
root<common_chat_schema_boolean>(t, doc_bool);
|
||||
auto doc_num = parse(R"({"type": "number", "minimum": 1, "maximum": 2})");
|
||||
root<common_chat_schema_number>(t, doc_num);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_integer(testing & t) {
|
||||
t.test("unbounded", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer"})");
|
||||
const auto & i = root<common_chat_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", INT64_MIN, i.minimum);
|
||||
t.assert_equal("maximum", INT64_MAX, i.maximum);
|
||||
});
|
||||
|
||||
t.test("inclusive bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "minimum": -5, "maximum": 10})");
|
||||
const auto & i = root<common_chat_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", -5, i.minimum);
|
||||
t.assert_equal("maximum", 10, i.maximum);
|
||||
});
|
||||
|
||||
t.test("exclusive bounds are folded", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "exclusiveMinimum": 0, "exclusiveMaximum": 10})");
|
||||
const auto & i = root<common_chat_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 1, i.minimum);
|
||||
t.assert_equal("maximum", 9, i.maximum);
|
||||
});
|
||||
|
||||
t.test("fractional bounds round inwards", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "minimum": 1.5, "exclusiveMaximum": 9.5})");
|
||||
const auto & i = root<common_chat_schema_integer>(t, doc);
|
||||
t.assert_equal("minimum", 2, i.minimum);
|
||||
t.assert_equal("maximum", 9, i.maximum);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_string(testing & t) {
|
||||
t.test("defaults", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string"})");
|
||||
const auto & s = root<common_chat_schema_string>(t, doc);
|
||||
t.assert_equal("pattern", "", s.pattern);
|
||||
t.assert_equal("format", common_chat_schema::FORMAT_NONE, s.format);
|
||||
t.assert_equal("min_length", 0, s.min_length);
|
||||
t.assert_equal("max_length", -1, s.max_length);
|
||||
});
|
||||
|
||||
t.test("all keywords are kept", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string", "pattern": "^[a-z]+$", "format": "date", "minLength": 2, "maxLength": 8})");
|
||||
const auto & s = root<common_chat_schema_string>(t, doc);
|
||||
t.assert_equal("pattern", "^[a-z]+$", s.pattern);
|
||||
t.assert_equal("format", common_chat_schema::FORMAT_DATE, s.format);
|
||||
t.assert_equal("min_length", 2, s.min_length);
|
||||
t.assert_equal("max_length", 8, s.max_length);
|
||||
});
|
||||
|
||||
t.test("formats", [](testing & t) {
|
||||
auto expect = [&](const char * format, common_chat_schema::string_format expected) {
|
||||
auto doc = parse(std::string(R"({"type": "string", "format": ")") + format + "\"}");
|
||||
t.assert_equal(format, expected, root<common_chat_schema_string>(t, doc).format);
|
||||
};
|
||||
expect("time", common_chat_schema::FORMAT_TIME);
|
||||
expect("date-time", common_chat_schema::FORMAT_DATE_TIME);
|
||||
expect("uuid", common_chat_schema::FORMAT_UUID);
|
||||
expect("uuid5", common_chat_schema::FORMAT_UUID);
|
||||
expect("email", common_chat_schema::FORMAT_NONE);
|
||||
});
|
||||
|
||||
t.test("pattern, length and known format imply a string", [](testing & t) {
|
||||
auto doc_pattern = parse(R"({"pattern": "^a$"})");
|
||||
t.assert_equal("pattern", "^a$", root<common_chat_schema_string>(t, doc_pattern).pattern);
|
||||
auto doc_length = parse(R"({"minLength": 1, "maxLength": 3})");
|
||||
t.assert_equal("min_length", 1, root<common_chat_schema_string>(t, doc_length).min_length);
|
||||
t.assert_equal("max_length", 3, root<common_chat_schema_string>(t, doc_length).max_length);
|
||||
auto doc_format = parse(R"({"format": "uuid"})");
|
||||
t.assert_equal("format", common_chat_schema::FORMAT_UUID, root<common_chat_schema_string>(t, doc_format).format);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_array(testing & t) {
|
||||
t.test("items with bounds", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 3})");
|
||||
const auto & a = root<common_chat_schema_array>(t, doc);
|
||||
as<common_chat_schema_integer>(t, a.items.get(), "items");
|
||||
t.assert_equal("min_items", 1, a.min_items);
|
||||
t.assert_equal("max_items", 3, a.max_items);
|
||||
});
|
||||
|
||||
t.test("no items", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array"})");
|
||||
const auto & a = root<common_chat_schema_array>(t, doc);
|
||||
as<common_chat_schema_any>(t, a.items.get(), "items");
|
||||
t.assert_equal("min_items", 0, a.min_items);
|
||||
t.assert_equal("max_items", -1, a.max_items);
|
||||
});
|
||||
|
||||
t.test("items imply an array", [](testing & t) {
|
||||
auto doc = parse(R"({"items": {"type": "string"}})");
|
||||
const auto & a = root<common_chat_schema_array>(t, doc);
|
||||
as<common_chat_schema_string>(t, a.items.get(), "items");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_tuple(testing & t) {
|
||||
t.test("prefixItems", [](testing & t) {
|
||||
auto doc = parse(R"({"prefixItems": [{"type": "string"}, {"type": "number"}]})");
|
||||
const auto & tup = root<common_chat_schema_tuple>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, tup.items.size());
|
||||
as<common_chat_schema_string>(t, tup.items[0].get(), "items[0]");
|
||||
as<common_chat_schema_number>(t, tup.items[1].get(), "items[1]");
|
||||
});
|
||||
|
||||
t.test("items as an array", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "array", "items": [{"type": "boolean"}]})");
|
||||
const auto & tup = root<common_chat_schema_tuple>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, tup.items.size());
|
||||
as<common_chat_schema_boolean>(t, tup.items[0].get(), "items[0]");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_object(testing & t) {
|
||||
t.test("type alone accepts any object", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object"})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
t.assert_true("no properties", o.properties.empty());
|
||||
as<common_chat_schema_any>(t, o.additional_properties.get(), "additional_properties");
|
||||
});
|
||||
|
||||
t.test("properties", [](testing & t) {
|
||||
auto doc = parse(R"({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"b": {"type": "string"},
|
||||
"a": {"type": "integer"},
|
||||
"c": {"type": "boolean"}
|
||||
},
|
||||
"required": ["a", "c"]
|
||||
})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 3, o.properties.size());
|
||||
t.assert_equal("order", "b", o.properties[0].name);
|
||||
t.assert_equal("order", "a", o.properties[1].name);
|
||||
t.assert_equal("order", "c", o.properties[2].name);
|
||||
t.assert_true("b optional", !o.properties[0].required);
|
||||
t.assert_true("a required", o.properties[1].required);
|
||||
t.assert_true("c required", o.properties[2].required);
|
||||
as<common_chat_schema_string>(t, o.properties[0].schema.get(), "b");
|
||||
as<common_chat_schema_integer>(t, o.properties[1].schema.get(), "a");
|
||||
as<common_chat_schema_boolean>(t, o.properties[2].schema.get(), "c");
|
||||
t.assert_true("closed", o.additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("unknown required entries are ignored", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}, "required": ["a", "zzz", 1]})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, o.properties.size());
|
||||
t.assert_true("a required", o.properties[0].required);
|
||||
});
|
||||
|
||||
t.test("additionalProperties false implies an object", [](testing & t) {
|
||||
auto doc = parse(R"({"additionalProperties": false})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
t.assert_true("no properties", o.properties.empty());
|
||||
t.assert_true("closed", o.additional_properties == nullptr);
|
||||
});
|
||||
|
||||
t.test("additionalProperties schema", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {}}, "additionalProperties": {"type": "integer", "minimum": 0}})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, o.properties.size());
|
||||
const auto & v = as<common_chat_schema_integer>(t, o.additional_properties.get(), "additional_properties");
|
||||
t.assert_equal("minimum", 0, v.minimum);
|
||||
});
|
||||
|
||||
t.test("nested", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"inner": {"properties": {"leaf": {"type": "null"}}, "required": ["leaf"]}}})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
const auto & inner = as<common_chat_schema_object>(t, o.properties[0].schema.get(), "inner");
|
||||
t.assert_equal("leaf name", "leaf", inner.properties[0].name);
|
||||
t.assert_true("leaf required", inner.properties[0].required);
|
||||
as<common_chat_schema_null>(t, inner.properties[0].schema.get(), "leaf");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_const_enum(testing & t) {
|
||||
t.test("const", [](testing & t) {
|
||||
auto doc = parse(R"({"const": {"a": [1, null]}})");
|
||||
t.assert_equal("value", R"({"a":[1,null]})", root<common_chat_schema_const>(t, doc).value.dump());
|
||||
});
|
||||
|
||||
t.test("enum", [](testing & t) {
|
||||
auto doc = parse(R"({"enum": ["a", 1, null, true]})");
|
||||
const auto & e = root<common_chat_schema_enum>(t, doc);
|
||||
t.assert_equal("size", (size_t) 4, e.values.size());
|
||||
t.assert_equal("values[0]", "\"a\"", e.values[0].dump());
|
||||
t.assert_equal("values[1]", "1", e.values[1].dump());
|
||||
t.assert_equal("values[2]", "null", e.values[2].dump());
|
||||
t.assert_equal("values[3]", "true", e.values[3].dump());
|
||||
});
|
||||
|
||||
t.test("const wins over enum, enum wins over type", [](testing & t) {
|
||||
auto doc_enum = parse(R"({"type": "integer", "enum": [1, 2]})");
|
||||
root<common_chat_schema_enum>(t, doc_enum);
|
||||
auto doc_const = parse(R"({"type": "string", "const": "x", "enum": ["y"]})");
|
||||
t.assert_equal("value", "\"x\"", root<common_chat_schema_const>(t, doc_const).value.dump());
|
||||
});
|
||||
}
|
||||
|
||||
static void test_any_of(testing & t) {
|
||||
t.test("anyOf and oneOf", [](testing & t) {
|
||||
auto doc_any = parse(R"({"anyOf": [{"type": "string"}, {"type": "number"}]})");
|
||||
const auto & u = root<common_chat_schema_any_of>(t, doc_any);
|
||||
t.assert_equal("size", (size_t) 2, u.children.size());
|
||||
as<common_chat_schema_string>(t, u.children[0].get(), "children[0]");
|
||||
as<common_chat_schema_number>(t, u.children[1].get(), "children[1]");
|
||||
|
||||
auto doc_one = parse(R"({"oneOf": [{"type": "null"}]})");
|
||||
const auto & o = root<common_chat_schema_any_of>(t, doc_one);
|
||||
t.assert_equal("size", (size_t) 1, o.children.size());
|
||||
as<common_chat_schema_null>(t, o.children[0].get(), "children[0]");
|
||||
});
|
||||
|
||||
t.test("oneOf wins over anyOf and type", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "string", "oneOf": [{"type": "null"}], "anyOf": [{"type": "number"}, {"type": "boolean"}]})");
|
||||
const auto & u = root<common_chat_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 1, u.children.size());
|
||||
as<common_chat_schema_null>(t, u.children[0].get(), "children[0]");
|
||||
});
|
||||
|
||||
t.test("type array expands with sibling keywords", [](testing & t) {
|
||||
auto doc = parse(R"({"type": ["string", "null", "integer"], "minLength": 2, "minimum": 5})");
|
||||
const auto & u = root<common_chat_schema_any_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 3, u.children.size());
|
||||
t.assert_equal("min_length", 2, as<common_chat_schema_string>(t, u.children[0].get(), "children[0]").min_length);
|
||||
as<common_chat_schema_null>(t, u.children[1].get(), "children[1]");
|
||||
t.assert_equal("minimum", 5, as<common_chat_schema_integer>(t, u.children[2].get(), "children[2]").minimum);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_all_of(testing & t) {
|
||||
t.test("components", [](testing & t) {
|
||||
auto doc = parse(R"({"allOf": [{"properties": {"a": {}}}, {"anyOf": [{"properties": {"b": {}}}, {"type": "null"}]}]})");
|
||||
const auto & all = root<common_chat_schema_all_of>(t, doc);
|
||||
t.assert_equal("size", (size_t) 2, all.children.size());
|
||||
as<common_chat_schema_object>(t, all.children[0].get(), "children[0]");
|
||||
as<common_chat_schema_any_of>(t, all.children[1].get(), "children[1]");
|
||||
|
||||
auto doc_typed = parse(R"({"type": "object", "allOf": [{"properties": {"a": {}}}]})");
|
||||
root<common_chat_schema_all_of>(t, doc_typed);
|
||||
});
|
||||
|
||||
t.test("properties win over allOf", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "object", "properties": {"a": {}}, "allOf": [{"properties": {"b": {}}}]})");
|
||||
t.assert_equal("size", (size_t) 1, root<common_chat_schema_object>(t, doc).properties.size());
|
||||
});
|
||||
|
||||
t.test("other types ignore allOf", [](testing & t) {
|
||||
auto doc = parse(R"({"type": "integer", "allOf": [{"minimum": 1}]})");
|
||||
root<common_chat_schema_integer>(t, doc);
|
||||
});
|
||||
}
|
||||
|
||||
static void test_ref(testing & t) {
|
||||
t.test("target is owned by the document", [](testing & t) {
|
||||
auto doc = parse(R"({"$ref": "#/$defs/t", "type": "string", "$defs": {"t": {"type": "boolean"}}})");
|
||||
const auto & r = root<common_chat_schema_ref>(t, doc);
|
||||
t.assert_equal("ref", "#/$defs/t", r.ref);
|
||||
t.assert_equal("refs", (size_t) 1, doc.refs.size());
|
||||
t.assert_true("target", r.target != nullptr && r.target == doc.refs.at("#/$defs/t").get());
|
||||
as<common_chat_schema_boolean>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("definitions", [](testing & t) {
|
||||
auto doc = parse(R"({"properties": {"a": {"$ref": "#/definitions/t"}}, "definitions": {"t": {"type": "number"}}})");
|
||||
const auto & o = root<common_chat_schema_object>(t, doc);
|
||||
const auto & r = as<common_chat_schema_ref>(t, o.properties[0].schema.get(), "a");
|
||||
as<common_chat_schema_number>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("recursive", [](testing & t) {
|
||||
auto doc = parse(R"({
|
||||
"$ref": "#/$defs/node",
|
||||
"$defs": {
|
||||
"node": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {"type": "number"},
|
||||
"next": {"$ref": "#/$defs/node"}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
}
|
||||
})");
|
||||
const auto & r = root<common_chat_schema_ref>(t, doc);
|
||||
const auto & node = as<common_chat_schema_object>(t, r.target, "node");
|
||||
t.assert_equal("properties", (size_t) 2, node.properties.size());
|
||||
const auto & next = as<common_chat_schema_ref>(t, node.properties[1].schema.get(), "next");
|
||||
t.assert_true("cycle", next.target == r.target);
|
||||
t.assert_equal("refs", (size_t) 1, doc.refs.size());
|
||||
});
|
||||
|
||||
t.test("pointer through an array", [](testing & t) {
|
||||
auto doc = parse(R"({"oneOf": [{"type": "null"}, {"$ref": "#/oneOf/0"}]})");
|
||||
const auto & u = root<common_chat_schema_any_of>(t, doc);
|
||||
const auto & r = as<common_chat_schema_ref>(t, u.children[1].get(), "children[1]");
|
||||
as<common_chat_schema_null>(t, r.target, "target");
|
||||
});
|
||||
|
||||
t.test("targets survive moving the document", [](testing & t) {
|
||||
auto parsed = parse(R"({"items": {"$ref": "#/$defs/t"}, "$defs": {"t": {"type": "null"}}})");
|
||||
common_chat_schema_document doc = std::move(parsed);
|
||||
const auto & a = root<common_chat_schema_array>(t, doc);
|
||||
const auto & r = as<common_chat_schema_ref>(t, a.items.get(), "items");
|
||||
t.assert_true("target", r.target == doc.refs.at("#/$defs/t").get());
|
||||
as<common_chat_schema_null>(t, r.target, "target");
|
||||
});
|
||||
}
|
||||
|
||||
static void test_may_be_string(testing & t) {
|
||||
auto check = [](testing & t, const std::string & schema, bool expected) {
|
||||
t.assert_equal(schema, expected, parse(schema).root->may_be_string());
|
||||
};
|
||||
|
||||
t.test("leaves", [&](testing & t) {
|
||||
check(t, R"({"type": "string"})", true);
|
||||
check(t, R"({"type": "integer"})", false);
|
||||
check(t, R"({"minLength": 1})", true);
|
||||
check(t, R"({"pattern": "^[a-z]+$"})", true);
|
||||
check(t, R"({"const": "hello"})", true);
|
||||
check(t, R"({"const": 123})", false);
|
||||
check(t, R"({"enum": [1, "a", null]})", true);
|
||||
check(t, R"({"enum": [1, 2, 3]})", false);
|
||||
});
|
||||
|
||||
t.test("composites", [&](testing & t) {
|
||||
check(t, R"({"type": ["integer", "string"]})", true);
|
||||
check(t, R"({"anyOf": [{"type": "integer"}, {"type": "boolean"}]})", false);
|
||||
check(t, R"({"allOf": [{"type": "string"}, {"minLength": 1}]})", true);
|
||||
check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", false);
|
||||
check(t, R"({"allOf": [{"minLength": 1}, {"maxLength": 2}]})", true);
|
||||
});
|
||||
|
||||
t.test("ref", [&](testing & t) {
|
||||
check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})", true);
|
||||
check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"$ref": "#/$defs/n"}}})", false);
|
||||
check(t, R"({"anyOf": [{"$ref": "#/$defs/a"}, {"$ref": "#/$defs/b"}], "$defs": {"a": {"allOf": [{"$ref": "#/$defs/b"}, {"type": "integer"}]}, "b": {"type": "string"}}})", true);
|
||||
});
|
||||
}
|
||||
|
||||
// e.g. {number, integer}, in type order
|
||||
static std::string dump(const common_chat_schema::type_set & types) {
|
||||
static const common_chat_schema::value_type order[] = { common_chat_schema::TYPE_NULL, common_chat_schema::TYPE_BOOLEAN, common_chat_schema::TYPE_NUMBER,
|
||||
common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_ARRAY,
|
||||
common_chat_schema::TYPE_OBJECT };
|
||||
std::string out;
|
||||
for (auto type : order) {
|
||||
if (types.has(type)) {
|
||||
out += (out.empty() ? "" : ", ") + std::string(common_chat_schema::type_name(type));
|
||||
}
|
||||
}
|
||||
return "{" + out + "}";
|
||||
}
|
||||
|
||||
static void test_value_types(testing & t) {
|
||||
auto check = [](testing & t, const std::string & schema, const common_chat_schema::type_set & expected) {
|
||||
t.assert_equal(schema, dump(expected), dump(parse(schema).root->value_types()));
|
||||
};
|
||||
|
||||
t.test("leaves", [&](testing & t) {
|
||||
check(t, R"({"type": "string"})", { common_chat_schema::TYPE_STRING });
|
||||
check(t, R"({"type": "number"})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER });
|
||||
check(t, R"({"description": "x"})", common_chat_schema::type_set::all());
|
||||
check(t, R"({"properties": {"a": {"type": "string"}}})", { common_chat_schema::TYPE_OBJECT });
|
||||
check(t, R"({"items": {"type": "string"}})", { common_chat_schema::TYPE_ARRAY });
|
||||
check(t, R"({"const": 1.5})", { common_chat_schema::TYPE_NUMBER });
|
||||
check(t, R"({"enum": [1, "a", null]})", { common_chat_schema::TYPE_INTEGER, common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL });
|
||||
});
|
||||
|
||||
t.test("any_of is the union, all_of is the intersection", [&](testing & t) {
|
||||
check(t, R"({"type": ["string", "null"]})", { common_chat_schema::TYPE_STRING, common_chat_schema::TYPE_NULL });
|
||||
check(t, R"({"allOf": [{"type": ["string", "number"]}, {"type": ["number", "object"]}]})", { common_chat_schema::TYPE_NUMBER, common_chat_schema::TYPE_INTEGER });
|
||||
check(t, R"({"allOf": [{"type": "string"}, {"type": "integer"}]})", {});
|
||||
});
|
||||
|
||||
t.test("ref", [&](testing & t) {
|
||||
check(t, R"({"$ref": "#/$defs/n", "$defs": {"n": {"anyOf": [{"$ref": "#/$defs/n"}, {"type": "string"}]}}})",
|
||||
{ common_chat_schema::TYPE_STRING });
|
||||
});
|
||||
}
|
||||
|
||||
static void test_errors(testing & t) {
|
||||
t.test("not a schema", [](testing & t) {
|
||||
assert_error(t, R"([])", "#: schema must be an object");
|
||||
});
|
||||
|
||||
t.test("type", [](testing & t) {
|
||||
assert_error(t, R"({"type": 5})", "#: type must be a string or an array of strings");
|
||||
assert_error(t, R"({"type": []})", "#: type must not be empty");
|
||||
assert_error(t, R"({"type": ["string", "bad"]})", "#/type/1: unrecognized type bad");
|
||||
});
|
||||
|
||||
t.test("ref", [](testing & t) {
|
||||
assert_error(t, R"({"$ref": 5})", "#: $ref must be a string");
|
||||
assert_error(t, R"({"$ref": "https://example.com/x.json"})", "#: unsupported $ref https://example.com/x.json");
|
||||
assert_error(t, R"({"$ref": ""})", "#: unsupported $ref ,");
|
||||
assert_error(t, R"({"$ref": "#"})", "#: unsupported $ref #,");
|
||||
assert_error(t, R"({"$defs": {}, "$ref": "#/$defs/missing"})", "#: cannot resolve $ref #/$defs/missing, missing not found");
|
||||
assert_error(t, R"({"oneOf": [{}], "$ref": "#/oneOf/1"})", "#: cannot resolve $ref #/oneOf/1, 1 is out of range");
|
||||
assert_error(t, R"({"$defs": {"a": {"$ref": "#/$defs/a/nope"}}, "$ref": "#/$defs/a"})", "#/$defs/a: cannot resolve $ref #/$defs/a/nope, nope not found");
|
||||
});
|
||||
|
||||
t.test("alternatives", [](testing & t) {
|
||||
assert_error(t, R"({"oneOf": []})", "#/oneOf: must not be empty");
|
||||
assert_error(t, R"({"anyOf": {}})", "#/anyOf: must be an array of schemas");
|
||||
assert_error(t, R"({"anyOf": [{"type": "string"}, {"items": {"type": "x"}}]})", "#/anyOf/1/items: unrecognized type x");
|
||||
});
|
||||
|
||||
t.test("keywords", [](testing & t) {
|
||||
assert_error(t, R"({"enum": []})", "#: enum must be a non-empty array");
|
||||
assert_error(t, R"({"type": "string", "pattern": 5})", "#: pattern must be a string");
|
||||
assert_error(t, R"({"type": "string", "minLength": -1})", "#: minLength must be a non-negative integer");
|
||||
assert_error(t, R"({"type": "integer", "minimum": "1"})", "#: minimum must be a number");
|
||||
assert_error(t, R"({"type": "array", "maxItems": 1.5})", "#: maxItems must be a non-negative integer");
|
||||
assert_error(t, R"({"properties": []})", "#: properties must be an object");
|
||||
assert_error(t, R"({"properties": {"a": {"type": "nope"}}})", "#/properties/a: unrecognized type nope");
|
||||
assert_error(t, R"({"additionalProperties": null})", "#: additionalProperties must be a boolean or a schema");
|
||||
});
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
testing t(std::cout);
|
||||
if (argc >= 2) {
|
||||
t.set_filter(argv[1]);
|
||||
}
|
||||
|
||||
const char * verbose = getenv("LLAMA_TEST_VERBOSE");
|
||||
if (verbose) {
|
||||
t.verbose = std::string(verbose) == "1";
|
||||
}
|
||||
|
||||
t.test("any", test_any);
|
||||
t.test("primitives", test_primitives);
|
||||
t.test("integer", test_integer);
|
||||
t.test("string", test_string);
|
||||
t.test("array", test_array);
|
||||
t.test("tuple", test_tuple);
|
||||
t.test("object", test_object);
|
||||
t.test("const and enum", test_const_enum);
|
||||
t.test("any_of", test_any_of);
|
||||
t.test("all_of", test_all_of);
|
||||
t.test("ref", test_ref);
|
||||
t.test("may_be_string", test_may_be_string);
|
||||
t.test("value_types", test_value_types);
|
||||
t.test("errors", test_errors);
|
||||
|
||||
return t.summary();
|
||||
}
|
||||
+2
-2
@@ -133,8 +133,8 @@
|
||||
| `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' |
|
||||
| `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) |
|
||||
| `--grammar-file FNAME` | file to read grammar from |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) |
|
||||
|
||||
|
||||
|
||||
@@ -216,8 +216,8 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1
|
||||
| `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' |
|
||||
| `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) |
|
||||
| `--grammar-file FNAME` | file to read grammar from |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) |
|
||||
|
||||
|
||||
@@ -556,7 +556,7 @@ These options help improve the performance and memory usage of the LLaMA models.
|
||||
|
||||
- `--grammar GRAMMAR`, `--grammar-file FILE`: Specify a grammar (defined inline or in a file) to constrain model output to a specific format. For example, you could force the model to output JSON or to speak only in emojis. See the [GBNF guide](../../grammars/README.md) for details on the syntax.
|
||||
|
||||
- `--json-schema SCHEMA`: Specify a [JSON schema](https://json-schema.org/) to constrain model output to (e.g. `{}` for any JSON object, or `{"items": {"type": "string", "minLength": 10, "maxLength": 100}, "minItems": 10}` for a JSON array of strings with size constraints). If a schema uses external `$ref`s, you should use `--grammar "$( python examples/json_schema_to_grammar.py myschema.json )"` instead.
|
||||
- `--json-schema SCHEMA`: Specify a [JSON schema](https://json-schema.org/) to constrain model output to (e.g. `{"type": "object"}` for any JSON object, or `{"items": {"type": "string", "minLength": 10, "maxLength": 100}, "minItems": 10}` for a JSON array of strings with size constraints).
|
||||
|
||||
### Quantization
|
||||
|
||||
|
||||
@@ -150,8 +150,8 @@ For the full list of features, please refer to [server's changelog](https://gith
|
||||
| `-l, --logit-bias TOKEN_ID(+/-)BIAS` | modifies the likelihood of token appearing in the completion,<br/>i.e. `--logit-bias 15043+1` to increase likelihood of token ' Hello',<br/>or `--logit-bias 15043-1` to decrease likelihood of token ' Hello' |
|
||||
| `--grammar GRAMMAR` | BNF-like grammar to constrain generations (see samples in grammars/ dir) |
|
||||
| `--grammar-file FNAME` | file to read grammar from |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{}` for any JSON object<br/>For schemas w/ external $refs, use --grammar + example/json_schema_to_grammar.py instead |
|
||||
| `-j, --json-schema SCHEMA` | JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-jf, --json-schema-file FILE` | File containing a JSON schema to constrain generations (https://json-schema.org/), e.g. `{"type": "object"}` for any JSON object |
|
||||
| `-bs, --backend-sampling` | enable backend sampling (experimental) (default: disabled)<br/>(env: LLAMA_ARG_BACKEND_SAMPLING) |
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <limits>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#ifdef _WIN32
|
||||
// windows.h defines min and max as macros, which breaks std::min and std::max
|
||||
@@ -1196,6 +1198,11 @@ json oaicompat_chat_params_parse(
|
||||
}
|
||||
}
|
||||
|
||||
// an absent or empty schema means any object
|
||||
if (json_schema.is_object() && json_schema.empty()) {
|
||||
json_schema["type"] = "object";
|
||||
}
|
||||
|
||||
// get input files
|
||||
if (!body.contains("messages")) {
|
||||
throw std::invalid_argument("'messages' is required");
|
||||
|
||||
@@ -1129,7 +1129,8 @@ void server_models::load(const std::string & name, const load_options & opts) {
|
||||
// exceeding models_max. Without this, the window between unload_lru()
|
||||
// releasing its lock and this lock_guard acquiring allows multiple
|
||||
// threads to each observe capacity and all proceed to load.
|
||||
if (base_params.models_max > 0) {
|
||||
// Download workers do not use models_max slots.
|
||||
if (opts.mode == SERVER_CHILD_MODE_NORMAL && base_params.models_max > 0) {
|
||||
size_t count_active = 0;
|
||||
for (const auto & m : mapping) {
|
||||
if (m.second.meta.is_running()) {
|
||||
|
||||
@@ -257,6 +257,10 @@ std::vector<std::unique_ptr<field>> make_llama_cmpl_schema(const common_params &
|
||||
if (data.contains("json_schema") && !data.contains("grammar")) {
|
||||
try {
|
||||
auto schema = json_value(data, "json_schema", json::object());
|
||||
if (schema.is_object() && schema.empty()) {
|
||||
// an empty schema means any object
|
||||
schema["type"] = "object";
|
||||
}
|
||||
SRV_DBG("JSON schema: %s\n", schema.dump(2).c_str());
|
||||
std::string grammar_str = json_schema_to_grammar(schema);
|
||||
SRV_DBG("Converted grammar: %s\n", grammar_str.c_str());
|
||||
|
||||
@@ -540,13 +540,17 @@ def _wait_for_sse_event(collected: list, event_type: str, model: str, timeout: i
|
||||
|
||||
|
||||
def test_router_download_model():
|
||||
"""Case 1: download a model, verify SSE events and GET /models."""
|
||||
"""Case 1: download a model at the model limit, verify SSE events and GET /models."""
|
||||
global server
|
||||
server.models_max = 1
|
||||
server.start()
|
||||
|
||||
# Ensure the model is not present before we start
|
||||
server.make_request("DELETE", f"/models?model={MODEL_DOWNLOAD_ID}")
|
||||
|
||||
# A download worker must not consume or evict a model slot
|
||||
_load_model_and_wait(MODEL_B, timeout=120)
|
||||
|
||||
sse_events: list = []
|
||||
stop = threading.Event()
|
||||
sse_ready = threading.Event()
|
||||
@@ -580,6 +584,7 @@ def test_router_download_model():
|
||||
# Model should now appear in GET /models
|
||||
ids = _get_model_ids(is_reload=False)
|
||||
assert MODEL_DOWNLOAD_ID in ids, f"{MODEL_DOWNLOAD_ID} not found in /models after download"
|
||||
assert _get_model_status(MODEL_B) == "loaded"
|
||||
|
||||
|
||||
def test_router_delete_model():
|
||||
|
||||
Vendored
+217
-79
@@ -912,17 +912,42 @@ bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
|
||||
namespace ws {
|
||||
namespace impl {
|
||||
|
||||
bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
std::string &payload, bool &fin,
|
||||
bool expect_masked, size_t max_len) {
|
||||
// Read first 2 bytes
|
||||
// Read exactly `size` bytes. Stream::read may return less than asked for -- it
|
||||
// hands back whatever its buffer already holds -- so every multi-byte field has
|
||||
// to loop. Reading a 2-byte header with a single read() fails whenever the
|
||||
// header straddles the read buffer's boundary.
|
||||
//
|
||||
// Timeout is reported only when nothing at all was consumed. Once a byte has
|
||||
// been taken the stream sits mid-field and cannot be resumed, so a timeout
|
||||
// there is a failure like any other. (When read() fails it always records why,
|
||||
// so the error belongs to this call and not to an earlier one.)
|
||||
FrameRead read_exact(Stream &strm, void *buf, size_t size) {
|
||||
auto p = static_cast<char *>(buf);
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
auto n = strm.read(p + total, size - total);
|
||||
if (n <= 0) {
|
||||
auto timed_out = total == 0 && strm.get_error() == Error::Timeout;
|
||||
return timed_out ? FrameRead::Timeout : FrameRead::Fail;
|
||||
}
|
||||
total += static_cast<size_t>(n);
|
||||
}
|
||||
return FrameRead::Ok;
|
||||
}
|
||||
|
||||
FrameRead read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
std::string &payload, bool &fin,
|
||||
bool expect_masked, size_t max_len) {
|
||||
// Read first 2 bytes. This is the only read that may report a timeout: it
|
||||
// sits on a frame boundary, where nothing has been consumed yet.
|
||||
uint8_t header[2];
|
||||
if (strm.read(reinterpret_cast<char *>(header), 2) != 2) { return false; }
|
||||
FrameRead first = read_exact(strm, header, 2);
|
||||
if (first != FrameRead::Ok) { return first; }
|
||||
|
||||
fin = (header[0] & 0x80) != 0;
|
||||
|
||||
// RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
|
||||
if (header[0] & 0x70) { return false; }
|
||||
if (header[0] & 0x70) { return FrameRead::Fail; }
|
||||
|
||||
opcode = static_cast<Opcode>(header[0] & 0x0F);
|
||||
bool masked = (header[1] & 0x80) != 0;
|
||||
@@ -932,46 +957,44 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
// MUST have a payload length of 125 bytes or less
|
||||
bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0;
|
||||
if (is_control) {
|
||||
if (!fin) { return false; }
|
||||
if (payload_len > 125) { return false; }
|
||||
if (!fin) { return FrameRead::Fail; }
|
||||
if (payload_len > 125) { return FrameRead::Fail; }
|
||||
}
|
||||
|
||||
if (masked != expect_masked) { return false; }
|
||||
if (masked != expect_masked) { return FrameRead::Fail; }
|
||||
|
||||
// Extended payload length
|
||||
if (payload_len == 126) {
|
||||
uint8_t ext[2];
|
||||
if (strm.read(reinterpret_cast<char *>(ext), 2) != 2) { return false; }
|
||||
if (read_exact(strm, ext, 2) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||
payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
|
||||
} else if (payload_len == 127) {
|
||||
uint8_t ext[8];
|
||||
if (strm.read(reinterpret_cast<char *>(ext), 8) != 8) { return false; }
|
||||
if (read_exact(strm, ext, 8) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||
// RFC 6455 Section 5.2: the most significant bit MUST be 0
|
||||
if (ext[0] & 0x80) { return false; }
|
||||
if (ext[0] & 0x80) { return FrameRead::Fail; }
|
||||
payload_len = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
payload_len = (payload_len << 8) | ext[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (payload_len > max_len) { return false; }
|
||||
if (payload_len > max_len) { return FrameRead::Fail; }
|
||||
|
||||
// Read mask key if present
|
||||
uint8_t mask_key[4] = {0};
|
||||
if (masked) {
|
||||
if (strm.read(reinterpret_cast<char *>(mask_key), 4) != 4) { return false; }
|
||||
if (read_exact(strm, mask_key, 4) != FrameRead::Ok) {
|
||||
return FrameRead::Fail;
|
||||
}
|
||||
}
|
||||
|
||||
// Read payload
|
||||
payload.resize(static_cast<size_t>(payload_len));
|
||||
if (payload_len > 0) {
|
||||
size_t total_read = 0;
|
||||
while (total_read < payload_len) {
|
||||
auto n = strm.read(&payload[total_read],
|
||||
static_cast<size_t>(payload_len - total_read));
|
||||
if (n <= 0) { return false; }
|
||||
total_read += static_cast<size_t>(n);
|
||||
}
|
||||
if (payload_len > 0 &&
|
||||
read_exact(strm, &payload[0], static_cast<size_t>(payload_len)) !=
|
||||
FrameRead::Ok) {
|
||||
return FrameRead::Fail;
|
||||
}
|
||||
|
||||
// Unmask if needed
|
||||
@@ -981,7 +1004,7 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return FrameRead::Ok;
|
||||
}
|
||||
|
||||
} // namespace impl
|
||||
@@ -1728,7 +1751,9 @@ ssize_t select_impl(socket_t sock, short events, time_t sec,
|
||||
pfd.events = events;
|
||||
pfd.revents = 0;
|
||||
|
||||
auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
|
||||
// A negative timeout waits forever, poll's own convention. 0 keeps meaning
|
||||
// "return immediately", which callers here rely on to probe a socket.
|
||||
auto timeout = sec < 0 ? -1 : static_cast<int>(sec * 1000 + usec / 1000);
|
||||
|
||||
return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
|
||||
}
|
||||
@@ -1810,8 +1835,11 @@ private:
|
||||
bool ensure_readable();
|
||||
|
||||
socket_t sock_;
|
||||
time_t read_timeout_sec_;
|
||||
time_t read_timeout_usec_;
|
||||
// Atomic because ws::WebSocket::set_read_timeout() reaches this from another
|
||||
// thread while a read is in flight -- that is the point of it, for a caller
|
||||
// holding one connection and wanting control back to send on it.
|
||||
std::atomic<time_t> read_timeout_sec_;
|
||||
std::atomic<time_t> read_timeout_usec_;
|
||||
time_t write_timeout_sec_;
|
||||
time_t write_timeout_usec_;
|
||||
time_t max_timeout_msec_;
|
||||
@@ -2204,12 +2232,10 @@ int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
// actually finish before letting the stack frame go. The trade-off is that
|
||||
// a wedged DNS server can hold this thread for the system resolver timeout
|
||||
// (~30s by default) past the caller's connection timeout.
|
||||
struct gaicb request {};
|
||||
struct gaicb request{};
|
||||
struct gaicb *requests[1] = {&request};
|
||||
struct sigevent sevp {};
|
||||
struct timespec timeout {
|
||||
timeout_sec, 0
|
||||
};
|
||||
struct sigevent sevp{};
|
||||
struct timespec timeout{timeout_sec, 0};
|
||||
|
||||
request.ar_name = node;
|
||||
request.ar_service = service;
|
||||
@@ -2948,8 +2974,21 @@ EncodingType encoding_type(const Request &req,
|
||||
return best;
|
||||
}
|
||||
|
||||
// `content_type` is taken separately because a file-backed response has not
|
||||
// been given one yet when its coding has to be decided.
|
||||
EncodingType encoding_type(const Request &req, const Response &res,
|
||||
const std::string &content_type) {
|
||||
// The response already names a content coding of its own: a handler serving
|
||||
// a body it encoded itself (pre-compressed static assets, say), or a mount
|
||||
// point whose headers name the coding its files are stored in. Applying one
|
||||
// on top of that would double-encode the body and append a second
|
||||
// `Content-Encoding` field line.
|
||||
if (res.has_header("Content-Encoding")) { return EncodingType::None; }
|
||||
return encoding_type(req, content_type);
|
||||
}
|
||||
|
||||
EncodingType encoding_type(const Request &req, const Response &res) {
|
||||
return encoding_type(req, res.get_header_value("Content-Type"));
|
||||
return encoding_type(req, res, res.get_header_value("Content-Type"));
|
||||
}
|
||||
|
||||
std::unique_ptr<compressor> make_compressor(EncodingType type) {
|
||||
@@ -3677,6 +3716,17 @@ bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||
return case_ignore::equal(last_coding, "chunked");
|
||||
}
|
||||
|
||||
bool has_conflicting_content_length(const Headers &headers) {
|
||||
// RFC 9112 §6.3: a message carrying both Transfer-Encoding and a non-zero
|
||||
// Content-Length is framed ambiguously. The body readers here delimit it by
|
||||
// the transfer coding and drop Content-Length, while an intermediary may do
|
||||
// the reverse, so the two disagree on where the body ends and a reused
|
||||
// connection is desynchronised (request/response smuggling). Content-Length:
|
||||
// 0 is tolerated for compatibility with existing peers.
|
||||
return has_header(headers, "Transfer-Encoding") &&
|
||||
get_header_value_u64(headers, "Content-Length", 0, 0) > 0;
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
bool prepare_content_receiver(T &x, int &status,
|
||||
ContentReceiverWithProgress receiver,
|
||||
@@ -4035,7 +4085,7 @@ void set_file_content_provider(Response &res,
|
||||
return true;
|
||||
});
|
||||
|
||||
res.file_content_encoding_ = encoding;
|
||||
res.content_coding_ = encoding;
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
@@ -4361,13 +4411,20 @@ bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
||||
|
||||
ssize_t first = -1;
|
||||
if (!lhs.empty()) {
|
||||
ssize_t v;
|
||||
auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v);
|
||||
if (res.ec == std::errc{}) { first = v; }
|
||||
// Reject an overflowing first-byte-pos; treating it as absent (-1)
|
||||
// would turn the range into a suffix range.
|
||||
auto res =
|
||||
detail::from_chars(lhs.data(), lhs.data() + lhs.size(), first);
|
||||
if (res.ec != std::errc{}) {
|
||||
all_valid_ranges = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t last = -1;
|
||||
if (!rhs.empty()) {
|
||||
// An overflowing last-byte-pos is past any content length, so keeping
|
||||
// -1 ("remainder", RFC 9110 14.1.2) is correct here.
|
||||
ssize_t v;
|
||||
auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v);
|
||||
if (res.ec == std::errc{}) { last = v; }
|
||||
@@ -6902,7 +6959,7 @@ void Response::set_content(const char *s, size_t n,
|
||||
auto rng = headers.equal_range("Content-Type");
|
||||
headers.erase(rng.first, rng.second);
|
||||
set_header("Content-Type", content_type);
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content(const std::string &s,
|
||||
@@ -6917,7 +6974,7 @@ void Response::set_content(std::string &&s,
|
||||
auto rng = headers.equal_range("Content-Type");
|
||||
headers.erase(rng.first, rng.second);
|
||||
set_header("Content-Type", content_type);
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content_provider(
|
||||
@@ -6928,7 +6985,7 @@ void Response::set_content_provider(
|
||||
if (in_length > 0) { content_provider_ = std::move(provider); }
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content_provider(
|
||||
@@ -6939,7 +6996,7 @@ void Response::set_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_chunked_content_provider(
|
||||
@@ -6950,7 +7007,7 @@ void Response::set_chunked_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = true;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_file_content(const std::string &path,
|
||||
@@ -7991,12 +8048,19 @@ ssize_t WebSocketSSLStream::read(char *ptr, size_t size) {
|
||||
needs_readable || (err.code == tls::ErrorCode::SyscallError &&
|
||||
WSAGetLastError() == WSAETIMEDOUT);
|
||||
#endif
|
||||
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; }
|
||||
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) {
|
||||
error_ = Error::Read;
|
||||
return -1;
|
||||
}
|
||||
if (!(needs_readable ? wait_readable() : wait_writable())) {
|
||||
error_ = Error::Timeout;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Out of retries. Recording a reason matters: a caller that reads get_error()
|
||||
// to tell a timeout from a close would otherwise see whatever the previous
|
||||
// failure left behind (error_ is never cleared on success).
|
||||
error_ = Error::Read;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -8653,9 +8717,10 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
|
||||
}
|
||||
} else {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
auto type = detail::encoding_type(req, res);
|
||||
|
||||
auto compressor = detail::make_compressor(type);
|
||||
// Use the coding `apply_ranges()` chose when it wrote the headers;
|
||||
// re-negotiating here would disagree with them, e.g. once a handler's
|
||||
// own Content-Encoding header suppresses the negotiation.
|
||||
auto compressor = detail::make_compressor(res.content_coding_);
|
||||
if (!compressor) {
|
||||
compressor = detail::make_unique<detail::nocompressor>();
|
||||
}
|
||||
@@ -8881,7 +8946,8 @@ bool Server::handle_file_request(Request &req, Response &res) {
|
||||
auto encoding = detail::EncodingType::None;
|
||||
if (static_file_compression_) {
|
||||
content_type = content_type_of();
|
||||
encoding = static_file_encoding(req, content_type, stat.size());
|
||||
encoding =
|
||||
static_file_encoding(req, res, content_type, stat.size());
|
||||
}
|
||||
|
||||
// The ETag names the representation actually sent, so a client that
|
||||
@@ -9296,8 +9362,10 @@ bool Server::dispatch_request(Request &req, Response &res,
|
||||
// the ETag, which has to name the representation actually sent, and
|
||||
// `apply_static_file_compression()` go through this, so the two cannot drift
|
||||
// apart.
|
||||
detail::EncodingType Server::static_file_encoding(
|
||||
const Request &req, const std::string &content_type, size_t length) const {
|
||||
detail::EncodingType
|
||||
Server::static_file_encoding(const Request &req, const Response &res,
|
||||
const std::string &content_type,
|
||||
size_t length) const {
|
||||
if (!static_file_compression_) { return detail::EncodingType::None; }
|
||||
|
||||
// Nothing to compress, and an empty file already answers with
|
||||
@@ -9322,14 +9390,14 @@ detail::EncodingType Server::static_file_encoding(
|
||||
return detail::EncodingType::None;
|
||||
}
|
||||
|
||||
return detail::encoding_type(req, content_type);
|
||||
return detail::encoding_type(req, res, content_type);
|
||||
}
|
||||
|
||||
// Compresses a file-backed content provider into `res.body` and takes over the
|
||||
// framing headers. Returns false when the response is left untouched.
|
||||
bool Server::apply_static_file_compression(const Request &req,
|
||||
Response &res) const {
|
||||
auto type = res.file_content_encoding_;
|
||||
auto type = res.content_coding_;
|
||||
if (type == detail::EncodingType::None || !res.content_provider_) {
|
||||
return false;
|
||||
}
|
||||
@@ -9353,7 +9421,7 @@ bool Server::apply_static_file_compression(const Request &req,
|
||||
res.content_provider_success_ = true;
|
||||
res.content_provider_ = nullptr;
|
||||
res.content_length_ = 0;
|
||||
res.file_content_encoding_ = detail::EncodingType::None;
|
||||
res.content_coding_ = detail::EncodingType::None;
|
||||
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
@@ -9412,6 +9480,7 @@ void Server::apply_ranges(const Request &req, Response &res,
|
||||
if (res.content_provider_) {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
res.set_header("Transfer-Encoding", "chunked");
|
||||
res.content_coding_ = type;
|
||||
if (type != detail::EncodingType::None) {
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
@@ -9568,8 +9637,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// coding is not chunked, which leaves the body length undeterminable. The
|
||||
// latter must not fall through to the "no body" path, or the body bytes are
|
||||
// parsed as the next request on a persistent connection.
|
||||
if (req.has_header("Transfer-Encoding") &&
|
||||
(req.get_header_value_u64("Content-Length") > 0 ||
|
||||
if (detail::has_conflicting_content_length(req.headers) ||
|
||||
(req.has_header("Transfer-Encoding") &&
|
||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
@@ -9734,7 +9803,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
auto ws_strm =
|
||||
std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
||||
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0,
|
||||
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
ws::WebSocket ws(std::move(ws_strm), req, true,
|
||||
websocket_ping_interval_sec_,
|
||||
@@ -9744,7 +9813,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
}
|
||||
#endif
|
||||
// Use WebSocket-specific read timeout instead of HTTP timeout
|
||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND,
|
||||
0);
|
||||
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_);
|
||||
entry.handler(req, ws);
|
||||
@@ -9808,7 +9878,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
detail::set_file_content_provider(
|
||||
res, mm, content_type,
|
||||
static_file_encoding(req, content_type, mm->size()));
|
||||
static_file_encoding(req, res, content_type, mm->size()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10228,8 +10298,12 @@ Result ClientImpl::send_(Request &&req) {
|
||||
void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
|
||||
const std::string &ct) {
|
||||
(void)for_stream;
|
||||
for (const auto &header : default_headers_) {
|
||||
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
||||
// Default headers are meant for the origin and may carry its credentials, so
|
||||
// keep them off the CONNECT request the proxy reads.
|
||||
if (r.method != "CONNECT") {
|
||||
for (const auto &header : default_headers_) {
|
||||
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 9110 5.3 recommends sending control data such as Host first, so
|
||||
@@ -10379,6 +10453,17 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Same framing check as ClientImpl::process_request(). A HEAD or bodyless
|
||||
// (204/304) response legitimately carries framing headers with no body.
|
||||
if (method != "HEAD" &&
|
||||
handle.response->status != StatusCode::NoContent_204 &&
|
||||
handle.response->status != StatusCode::NotModified_304 &&
|
||||
detail::has_conflicting_content_length(handle.response->headers)) {
|
||||
handle.error = Error::Read;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
|
||||
handle.body_reader_.stream = handle.stream_;
|
||||
handle.body_reader_.payload_max_length = payload_max_length_;
|
||||
|
||||
@@ -10910,24 +10995,24 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
}
|
||||
}
|
||||
|
||||
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
|
||||
if (!req.has_header("Authorization")) {
|
||||
// A CONNECT request is read by the proxy; everything sent through the tunnel
|
||||
// it opens is read by the origin. Each credential goes only to its own hop.
|
||||
auto is_connect = req.method == "CONNECT";
|
||||
|
||||
if (!is_connect && !req.has_header("Authorization")) {
|
||||
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
|
||||
req.headers.insert(make_basic_authentication_header(
|
||||
basic_auth_username_, basic_auth_password_, false));
|
||||
}
|
||||
}
|
||||
|
||||
if (!bearer_token_auth_token_.empty()) {
|
||||
if (!req.has_header("Authorization")) {
|
||||
} else if (!bearer_token_auth_token_.empty()) {
|
||||
req.headers.insert(make_bearer_token_authentication_header(
|
||||
bearer_token_auth_token_, false));
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy-Authorization is only sent when the proxy is actually used for
|
||||
// this target — otherwise NO_PROXY-matched requests would leak proxy
|
||||
// credentials directly to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_)) {
|
||||
// Proxy-Authorization is only sent when the proxy reads this message —
|
||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||
// would leak proxy credentials to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||
if (!proxy_basic_auth_username_.empty() &&
|
||||
!proxy_basic_auth_password_.empty() &&
|
||||
!req.has_header("Proxy-Authorization")) {
|
||||
@@ -11323,6 +11408,17 @@ bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
// Body
|
||||
if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
|
||||
req.method != "CONNECT") {
|
||||
// Reject ambiguous framing (RFC 9112 §6.3). Unlike a request, a response
|
||||
// whose final transfer coding is not chunked is not ambiguous: its body
|
||||
// runs until the server closes the connection, so it is not rejected.
|
||||
// HEAD/204 are excluded above and a 304 carries no body.
|
||||
if (res.status != StatusCode::NotModified_304 &&
|
||||
detail::has_conflicting_content_length(res.headers)) {
|
||||
error = Error::Read;
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto redirect = 300 < res.status && res.status < 400 &&
|
||||
res.status != StatusCode::NotModified_304 &&
|
||||
follow_location_;
|
||||
@@ -17562,8 +17658,16 @@ ReadResult WebSocket::read(std::string &msg) {
|
||||
std::string payload;
|
||||
bool fin;
|
||||
|
||||
if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
||||
impl::FrameRead r =
|
||||
impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH);
|
||||
// A timeout landed on a frame boundary: the connection is untouched and
|
||||
// still usable, so hand control back without closing it. That is only
|
||||
// useful to a caller who asked for the timeout; the compile-time default
|
||||
// is a backstop against a peer gone quiet, and elapsing it closes the
|
||||
// connection so a plain `while (ws.read(msg))` loop ends.
|
||||
if (r == impl::FrameRead::Timeout && read_timeout_set_) { return Timeout; }
|
||||
if (r != impl::FrameRead::Ok) {
|
||||
closed_ = true;
|
||||
return Fail;
|
||||
}
|
||||
@@ -17600,9 +17704,14 @@ ReadResult WebSocket::read(std::string &msg) {
|
||||
Opcode cont_opcode;
|
||||
std::string cont_payload;
|
||||
bool cont_fin;
|
||||
if (!impl::read_websocket_frame(
|
||||
// A timeout is not reportable here: half of a fragmented message is
|
||||
// already in `msg` and read() has no way to resume it, so it is a
|
||||
// failure like any other. Timeouts are only ever seen on a message
|
||||
// boundary.
|
||||
if (impl::read_websocket_frame(
|
||||
strm_, cont_opcode, cont_payload, cont_fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) !=
|
||||
impl::FrameRead::Ok) {
|
||||
closed_ = true;
|
||||
return Fail;
|
||||
}
|
||||
@@ -17696,7 +17805,8 @@ void WebSocket::close(CloseStatus status, const std::string &reason) {
|
||||
Opcode op;
|
||||
std::string resp;
|
||||
bool fin;
|
||||
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) {
|
||||
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125) ==
|
||||
impl::FrameRead::Ok) {
|
||||
if (op == Opcode::Close) { break; }
|
||||
}
|
||||
}
|
||||
@@ -17741,6 +17851,15 @@ const Request &WebSocket::request() const { return req_; }
|
||||
|
||||
bool WebSocket::is_open() const { return !closed_; }
|
||||
|
||||
void WebSocket::set_read_timeout(time_t sec, time_t usec) {
|
||||
// 0 waits forever here, as it does for SO_RCVTIMEO. The stream waits with
|
||||
// poll(), where 0 would instead mean "return immediately", so hand it the
|
||||
// negative poll uses for an unbounded wait.
|
||||
if (sec == 0 && usec == 0) { sec = -1; }
|
||||
strm_.set_read_timeout(sec, usec);
|
||||
read_timeout_set_ = true;
|
||||
}
|
||||
|
||||
// WebSocketClient implementation
|
||||
WebSocketClient::WebSocketClient(
|
||||
const std::string &scheme_host_port_path, const Headers &headers)
|
||||
@@ -17843,6 +17962,16 @@ void WebSocketClient::shutdown_and_close() {
|
||||
bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
Error &error, int &ssl_error,
|
||||
uint64_t &ssl_backend_error) {
|
||||
// A read timeout of 0 means "wait forever", the way SO_RCVTIMEO reads it.
|
||||
// The streams wait with poll(), where 0 instead means "return immediately",
|
||||
// so they are given the negative poll uses for an unbounded wait.
|
||||
auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0;
|
||||
time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_;
|
||||
time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_;
|
||||
// The handshake belongs to establishing the connection, so an unset read
|
||||
// timeout leaves it bounded by the connection timeout instead of forever.
|
||||
time_t hs_sec = unbounded ? connection_timeout_sec_ : read_timeout_sec_;
|
||||
time_t hs_usec = unbounded ? connection_timeout_usec_ : read_timeout_usec_;
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (is_ssl_) {
|
||||
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
||||
@@ -17862,8 +17991,8 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
detail::ClientTlsSessionError tls_error;
|
||||
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
||||
server_certificate_verification_,
|
||||
read_timeout_sec_, read_timeout_usec_,
|
||||
&tls_error, options)) {
|
||||
hs_sec, hs_usec, &tls_error,
|
||||
options)) {
|
||||
error = tls_error.error;
|
||||
ssl_error = tls_error.ssl_error;
|
||||
ssl_backend_error = tls_error.backend_error;
|
||||
@@ -17871,17 +18000,19 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
}
|
||||
|
||||
strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||
sock_, tls_session_, read_timeout_sec_, read_timeout_usec_,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
sock_, tls_session_, strm_read_sec, strm_read_usec, write_timeout_sec_,
|
||||
write_timeout_usec_));
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
(void)error;
|
||||
(void)ssl_error;
|
||||
(void)ssl_backend_error;
|
||||
(void)hs_sec;
|
||||
(void)hs_usec;
|
||||
#endif
|
||||
strm = std::unique_ptr<Stream>(
|
||||
new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_,
|
||||
new detail::SocketStream(sock_, strm_read_sec, strm_read_usec,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
return true;
|
||||
}
|
||||
@@ -17951,6 +18082,9 @@ Result WebSocketClient::connect() {
|
||||
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||
websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_));
|
||||
// The stream was created with the timeout already; tell the WebSocket
|
||||
// whether it came from the caller, so read() knows to report it as Timeout.
|
||||
ws_->read_timeout_set_ = read_timeout_set_;
|
||||
return Result{Error::Success, upgrade.status, std::move(upgrade.headers)};
|
||||
}
|
||||
|
||||
@@ -17983,6 +18117,10 @@ const std::string &WebSocketClient::subprotocol() const {
|
||||
void WebSocketClient::set_read_timeout(time_t sec, time_t usec) {
|
||||
read_timeout_sec_ = sec;
|
||||
read_timeout_usec_ = usec;
|
||||
read_timeout_set_ = true;
|
||||
// The members above only seed the next connect(); read() consults the
|
||||
// stream, so an already-open connection has to be told directly.
|
||||
if (ws_) { ws_->set_read_timeout(sec, usec); }
|
||||
}
|
||||
|
||||
void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
|
||||
|
||||
Vendored
+80
-12
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.54.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003601"
|
||||
#define CPPHTTPLIB_VERSION "0.56.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003800"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -215,8 +215,36 @@
|
||||
#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 300
|
||||
// One macro used to set the read timeout for both sides. They want different
|
||||
// defaults: a client's read timeout is the caller's own tool (it waits forever
|
||||
// until asked not to), while a server keeps a ceiling that reclaims a worker
|
||||
// from a peer that has gone quiet. The old name still works and sets both.
|
||||
#ifdef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||
#pragma message( \
|
||||
"CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND is deprecated; define " \
|
||||
"CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND and/or " \
|
||||
"CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND instead")
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND
|
||||
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND \
|
||||
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||
#endif
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND
|
||||
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND \
|
||||
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// 0 waits forever. A read timeout is how a caller gets control back to send on
|
||||
// the same connection; it is not a liveness check (that is ping/pong). Only a
|
||||
// timeout set at runtime through set_read_timeout() is reported as
|
||||
// ws::Timeout; when one of these compile-time defaults elapses, read() returns
|
||||
// ws::Fail and closes the connection.
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND
|
||||
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 0
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND
|
||||
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 300
|
||||
#endif
|
||||
|
||||
#ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND
|
||||
@@ -1817,10 +1845,12 @@ struct Response {
|
||||
std::string file_content_path_;
|
||||
std::string file_content_content_type_;
|
||||
|
||||
// Content coding chosen for a file-backed content provider, decided once
|
||||
// where the file is opened so that the ETag and the body cannot disagree.
|
||||
// `EncodingType::None` for every other kind of response.
|
||||
detail::EncodingType file_content_encoding_ = detail::EncodingType::None;
|
||||
// Content coding chosen for the response body, decided once so that the
|
||||
// headers and the body cannot disagree: where the file is opened for a
|
||||
// file-backed content provider (keeping the ETag honest), and in
|
||||
// `apply_ranges()` for a chunked content provider. `EncodingType::None`
|
||||
// for every other kind of response.
|
||||
detail::EncodingType content_coding_ = detail::EncodingType::None;
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
@@ -2359,6 +2389,7 @@ private:
|
||||
|
||||
bool parse_request_line(const char *s, Request &req) const;
|
||||
detail::EncodingType static_file_encoding(const Request &req,
|
||||
const Response &res,
|
||||
const std::string &content_type,
|
||||
size_t length) const;
|
||||
bool apply_static_file_compression(const Request &req, Response &res) const;
|
||||
@@ -3663,6 +3694,9 @@ ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
|
||||
|
||||
EncodingType encoding_type(const Request &req, const std::string &content_type);
|
||||
|
||||
EncodingType encoding_type(const Request &req, const Response &res,
|
||||
const std::string &content_type);
|
||||
|
||||
EncodingType encoding_type(const Request &req, const Response &res);
|
||||
|
||||
class BufferStream final : public Stream {
|
||||
@@ -4345,7 +4379,11 @@ enum class CloseStatus : uint16_t {
|
||||
InternalError = 1011,
|
||||
};
|
||||
|
||||
enum ReadResult : int { Fail = 0, Text = 1, Binary = 2 };
|
||||
// Timeout is returned only when a read timeout was set and it elapsed before
|
||||
// any byte of a frame arrived: nothing was consumed and the connection is
|
||||
// still open, so the caller can send on it and read again. `msg` is left
|
||||
// untouched, so a `while (ws.read(msg))` loop must not treat it as a message.
|
||||
enum ReadResult : int { Fail = 0, Text = 1, Binary = 2, Timeout = 3 };
|
||||
|
||||
// Result of WebSocketClient::connect(). Truthy only when the WebSocket
|
||||
// upgrade handshake fully succeeded. On failure error() identifies the
|
||||
@@ -4405,6 +4443,18 @@ public:
|
||||
const Request &request() const;
|
||||
bool is_open() const;
|
||||
|
||||
// Bound how long read() waits before returning Timeout. 0 waits forever.
|
||||
// A server handler owns its connection's timeout this way; a client sets it
|
||||
// through WebSocketClient. Safe to call while another thread is in read().
|
||||
//
|
||||
// Only a timeout set here is reported as Timeout. The compile-time default
|
||||
// (CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND) is a backstop rather
|
||||
// than a request for control, so when it elapses read() returns Fail and
|
||||
// closes the connection, and `while (ws.read(msg))` ends as it always has.
|
||||
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||
template <class Rep, class Period>
|
||||
void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||
|
||||
private:
|
||||
friend class httplib::Server;
|
||||
friend class WebSocketClient;
|
||||
@@ -4440,6 +4490,10 @@ private:
|
||||
int max_missed_pongs_;
|
||||
int unacked_pings_ = 0;
|
||||
std::atomic<bool> closed_{false};
|
||||
// Set once the caller has bounded read() through set_read_timeout(). Until
|
||||
// then the timeout in effect is the compile-time default, and elapsing it
|
||||
// is a failure that closes the connection, not a Timeout.
|
||||
std::atomic<bool> read_timeout_set_{false};
|
||||
std::mutex write_mutex_;
|
||||
// Owned by whichever thread is parsing frames off strm_. Only one thread
|
||||
// may do so: read_websocket_frame() reads a payload until it has the whole
|
||||
@@ -4527,8 +4581,9 @@ private:
|
||||
bool is_valid_ = false;
|
||||
socket_t sock_ = INVALID_SOCKET;
|
||||
std::unique_ptr<WebSocket> ws_;
|
||||
time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND;
|
||||
time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND;
|
||||
time_t read_timeout_usec_ = 0;
|
||||
bool read_timeout_set_ = false; // see WebSocket::read_timeout_set_
|
||||
time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
|
||||
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
||||
time_t websocket_ping_interval_sec_ =
|
||||
@@ -4560,6 +4615,13 @@ private:
|
||||
#endif
|
||||
};
|
||||
|
||||
template <class Rep, class Period>
|
||||
inline void WebSocket::set_read_timeout(
|
||||
const std::chrono::duration<Rep, Period> &duration) {
|
||||
detail::duration_to_sec_and_usec(
|
||||
duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
|
||||
}
|
||||
|
||||
template <class Rep, class Period>
|
||||
inline void WebSocketClient::set_read_timeout(
|
||||
const std::chrono::duration<Rep, Period> &duration) {
|
||||
@@ -4586,8 +4648,14 @@ namespace impl {
|
||||
|
||||
bool is_valid_utf8(const std::string &s);
|
||||
|
||||
bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload,
|
||||
bool &fin, bool expect_masked, size_t max_len);
|
||||
// Three states, because a failure that consumed bytes and one that consumed
|
||||
// none are not the same thing: the first has left the stream in the middle of
|
||||
// a frame and the connection cannot be reused, the second can just be retried.
|
||||
enum class FrameRead { Ok, Fail, Timeout };
|
||||
|
||||
FrameRead read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
std::string &payload, bool &fin,
|
||||
bool expect_masked, size_t max_len);
|
||||
|
||||
} // namespace impl
|
||||
|
||||
|
||||
Reference in New Issue
Block a user