common : add nemotron 3 parsing (#18077)
* common : expose json-schema functionality to extract type info * common : fix peg parser negation during needs_more_input * common : add some defensive measures in constructed peg parser * common : add nemotron nano 3 support * common : add nemotron nano 3 tests * remove debug line
This commit is contained in:
parent
279cef27c2
commit
c05aa69f32
|
|
@ -4,9 +4,14 @@
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
static std::string_view trim_trailing_space(std::string_view sv) {
|
static std::string_view trim_trailing_space(std::string_view sv, int max = -1) {
|
||||||
|
int count = 0;
|
||||||
while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.back()))) {
|
while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.back()))) {
|
||||||
|
if (max != -1 && count <= max) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
sv.remove_suffix(1);
|
sv.remove_suffix(1);
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
return sv;
|
return sv;
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +98,7 @@ void common_chat_peg_constructed_mapper::map(const common_peg_ast_node & node) {
|
||||||
|
|
||||||
if (is_arg_string && current_tool) {
|
if (is_arg_string && current_tool) {
|
||||||
// Serialize to JSON, but exclude the end quote
|
// Serialize to JSON, but exclude the end quote
|
||||||
std::string dumped = json(node.text).dump();
|
std::string dumped = json(trim_trailing_space(node.text)).dump();
|
||||||
current_tool->arguments += dumped.substr(0, dumped.size() - 1);
|
current_tool->arguments += dumped.substr(0, dumped.size() - 1);
|
||||||
needs_closing_quote = true;
|
needs_closing_quote = true;
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +106,7 @@ void common_chat_peg_constructed_mapper::map(const common_peg_ast_node & node) {
|
||||||
if (is_arg_close && current_tool) {
|
if (is_arg_close && current_tool) {
|
||||||
if (needs_closing_quote) {
|
if (needs_closing_quote) {
|
||||||
current_tool->arguments += "\"";
|
current_tool->arguments += "\"";
|
||||||
|
needs_closing_quote = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +115,10 @@ void common_chat_peg_constructed_mapper::map(const common_peg_ast_node & node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_tool_close && current_tool) {
|
if (is_tool_close && current_tool) {
|
||||||
|
if (needs_closing_quote) {
|
||||||
|
current_tool->arguments += "\"";
|
||||||
|
needs_closing_quote = false;
|
||||||
|
}
|
||||||
current_tool->arguments += "}";
|
current_tool->arguments += "}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
140
common/chat.cpp
140
common/chat.cpp
|
|
@ -711,6 +711,25 @@ static void foreach_function(const json & tools, const std::function<void(const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static 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()) {
|
||||||
|
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()) {
|
||||||
|
params.at("required").get_to(required);
|
||||||
|
}
|
||||||
|
for (const auto & [name, prop] : props.items()) {
|
||||||
|
bool is_required = (required.find(name) != required.end());
|
||||||
|
fn(name, prop, is_required);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static std::string apply(
|
static std::string apply(
|
||||||
const common_chat_template & tmpl,
|
const common_chat_template & tmpl,
|
||||||
const struct templates_params & inputs,
|
const struct templates_params & inputs,
|
||||||
|
|
@ -1409,6 +1428,123 @@ static common_chat_params common_chat_params_init_nemotron_v2(const common_chat_
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static common_chat_params common_chat_params_init_nemotron_v3(const common_chat_template & tmpl, const struct templates_params & inputs) {
|
||||||
|
common_chat_params data;
|
||||||
|
|
||||||
|
data.prompt = apply(tmpl, inputs);
|
||||||
|
data.format = COMMON_CHAT_FORMAT_PEG_CONSTRUCTED;
|
||||||
|
|
||||||
|
// Handle thinking tags appropriately based on inputs.enable_thinking
|
||||||
|
if (string_ends_with(data.prompt, "<think>\n")) {
|
||||||
|
if (!inputs.enable_thinking) {
|
||||||
|
data.prompt += "</think>";
|
||||||
|
} else {
|
||||||
|
data.thinking_forced_open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.preserved_tokens = {
|
||||||
|
"<think>",
|
||||||
|
"</think>",
|
||||||
|
"<tool_call>",
|
||||||
|
"</tool_call>",
|
||||||
|
};
|
||||||
|
|
||||||
|
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||||
|
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||||
|
auto include_grammar = true;
|
||||||
|
|
||||||
|
auto parser = build_chat_peg_constructed_parser([&](auto & p) {
|
||||||
|
auto reasoning = p.eps();
|
||||||
|
if (inputs.enable_thinking && extract_reasoning) {
|
||||||
|
auto reasoning_content = p.reasoning(p.until("</think>")) + ("</think>" | p.end());
|
||||||
|
if (data.thinking_forced_open) {
|
||||||
|
reasoning = reasoning_content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response format parser
|
||||||
|
if (inputs.json_schema.is_object() && !inputs.json_schema.empty()) {
|
||||||
|
return reasoning << p.content(p.schema(p.json(), "response-format", inputs.json_schema));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool call parser
|
||||||
|
if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
|
||||||
|
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.at("parameters");
|
||||||
|
|
||||||
|
auto schema_info = common_schema_info();
|
||||||
|
schema_info.resolve_refs(parameters);
|
||||||
|
|
||||||
|
auto tool_open = "<function=" + p.tool_name(p.literal(name)) + ">\n";
|
||||||
|
auto tool_close = p.literal("</function>\n");
|
||||||
|
auto args = p.sequence();
|
||||||
|
auto arg_string = p.rule("xml-arg-string", p.until_one_of({
|
||||||
|
"\n</parameter>",
|
||||||
|
"\n<parameter=",
|
||||||
|
"\n</function>"
|
||||||
|
}));
|
||||||
|
|
||||||
|
foreach_parameter(function, [&](const auto & param_name, const json & param_schema, bool is_required) {
|
||||||
|
auto rule_name = "tool-" + name + "-arg-" + param_name;
|
||||||
|
|
||||||
|
auto arg_open = "<parameter=" + p.tool_arg_name(p.literal(param_name)) + ">\n";
|
||||||
|
auto arg_close = p.literal("</parameter>\n");
|
||||||
|
auto arg_value = p.eps();
|
||||||
|
|
||||||
|
if (schema_info.resolves_to_string(param_schema)) {
|
||||||
|
arg_value = p.tool_arg_string_value(arg_string) + "\n";
|
||||||
|
} else {
|
||||||
|
arg_value = p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", param_schema));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model may or my not close with </parameter>
|
||||||
|
auto arg_rule = p.rule(rule_name, p.tool_arg_open(arg_open) + arg_value + p.optional(p.tool_arg_close(arg_close)));
|
||||||
|
args += p.repeat(arg_rule, /* min = */ is_required ? 1 : 0, /* max = */ 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tool_choice |= p.rule("tool-" + name, p.tool_open(tool_open) + args + p.tool_close(tool_close));
|
||||||
|
});
|
||||||
|
|
||||||
|
auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0;
|
||||||
|
auto max_calls = inputs.parallel_tool_calls ? -1 : 1;
|
||||||
|
auto tool_call = p.rule("tool-call", "<tool_call>\n" + tool_choice + "</tool_call>" + p.space());
|
||||||
|
auto tool_calls = p.trigger_rule("tool-call-root", p.repeat(tool_call, /* min = */ min_calls, /* max = */ max_calls));
|
||||||
|
|
||||||
|
return reasoning << p.content(p.until("<tool_call>")) << tool_calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content only parser
|
||||||
|
include_grammar = false;
|
||||||
|
return reasoning << p.content(p.rest());
|
||||||
|
});
|
||||||
|
|
||||||
|
data.parser = parser.save();
|
||||||
|
|
||||||
|
if (include_grammar) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.grammar_triggers = {
|
||||||
|
{COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<tool_call>"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static common_chat_params common_chat_params_init_apertus(const common_chat_template & tmpl, const struct templates_params & inputs) {
|
static common_chat_params common_chat_params_init_apertus(const common_chat_template & tmpl, const struct templates_params & inputs) {
|
||||||
common_chat_params data;
|
common_chat_params data;
|
||||||
|
|
||||||
|
|
@ -2534,6 +2670,10 @@ static common_chat_params common_chat_templates_apply_jinja(
|
||||||
src.find("<function=") != std::string::npos &&
|
src.find("<function=") != std::string::npos &&
|
||||||
src.find("<parameters>") != std::string::npos &&
|
src.find("<parameters>") != std::string::npos &&
|
||||||
src.find("<parameter=") != std::string::npos) {
|
src.find("<parameter=") != std::string::npos) {
|
||||||
|
// Nemotron 3 Nano 30B A3B
|
||||||
|
if (src.find("<think>") != std::string::npos) {
|
||||||
|
return common_chat_params_init_nemotron_v3(tmpl, params);
|
||||||
|
}
|
||||||
return common_chat_params_init_qwen3_coder_xml(tmpl, params);
|
return common_chat_params_init_qwen3_coder_xml(tmpl, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -305,8 +305,9 @@ static std::string format_literal(const std::string & literal) {
|
||||||
|
|
||||||
std::string gbnf_format_literal(const std::string & literal) { return format_literal(literal); }
|
std::string gbnf_format_literal(const std::string & literal) { return format_literal(literal); }
|
||||||
|
|
||||||
class SchemaConverter {
|
class common_schema_converter {
|
||||||
private:
|
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);
|
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;
|
std::function<json(const std::string &)> _fetch_json;
|
||||||
bool _dotall;
|
bool _dotall;
|
||||||
|
|
@ -729,7 +730,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SchemaConverter(
|
common_schema_converter(
|
||||||
const std::function<json(const std::string &)> & fetch_json,
|
const std::function<json(const std::string &)> & fetch_json,
|
||||||
bool dotall)
|
bool dotall)
|
||||||
: _fetch_json(fetch_json), _dotall(dotall)
|
: _fetch_json(fetch_json), _dotall(dotall)
|
||||||
|
|
@ -990,6 +991,134 @@ 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(nlohmann::ordered_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 nlohmann::ordered_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 json & schema, bool force_gbnf) {
|
std::string json_schema_to_grammar(const json & schema, bool force_gbnf) {
|
||||||
#ifdef LLAMA_USE_LLGUIDANCE
|
#ifdef LLAMA_USE_LLGUIDANCE
|
||||||
if (!force_gbnf) {
|
if (!force_gbnf) {
|
||||||
|
|
@ -1006,7 +1135,7 @@ std::string json_schema_to_grammar(const json & schema, bool force_gbnf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options) {
|
std::string build_grammar(const std::function<void(const common_grammar_builder &)> & cb, const common_grammar_options & options) {
|
||||||
SchemaConverter converter([&](const std::string &) { return json(); }, options.dotall);
|
common_schema_converter converter([&](const std::string &) { return json(); }, options.dotall);
|
||||||
common_grammar_builder builder {
|
common_grammar_builder builder {
|
||||||
/* .add_rule = */ [&](const std::string & name, const std::string & rule) {
|
/* .add_rule = */ [&](const std::string & name, const std::string & rule) {
|
||||||
return converter._add_rule(name, rule);
|
return converter._add_rule(name, rule);
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,31 @@
|
||||||
#include <nlohmann/json_fwd.hpp>
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
std::string json_schema_to_grammar(const nlohmann::ordered_json & schema,
|
std::string json_schema_to_grammar(const nlohmann::ordered_json & schema,
|
||||||
bool force_gbnf = false);
|
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(nlohmann::ordered_json & schema);
|
||||||
|
bool resolves_to_string(const nlohmann::ordered_json & schema);
|
||||||
|
};
|
||||||
|
|
||||||
struct common_grammar_builder {
|
struct common_grammar_builder {
|
||||||
std::function<std::string(const std::string &, const std::string &)> add_rule;
|
std::function<std::string(const std::string &, const std::string &)> add_rule;
|
||||||
std::function<std::string(const std::string &, const nlohmann::ordered_json &)> add_schema;
|
std::function<std::string(const std::string &, const nlohmann::ordered_json &)> add_schema;
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,7 @@ struct parser_executor {
|
||||||
|
|
||||||
if (result.need_more_input()) {
|
if (result.need_more_input()) {
|
||||||
// Propagate - need to know what child would match before negating
|
// Propagate - need to know what child would match before negating
|
||||||
return result;
|
return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_NEED_MORE_INPUT, start_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Child failed, so negation succeeds
|
// Child failed, so negation succeeds
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
{% macro render_extra_keys(json_dict, handled_keys) %}
|
||||||
|
{%- if json_dict is mapping %}
|
||||||
|
{%- for json_key in json_dict if json_key not in handled_keys %}
|
||||||
|
{%- if json_dict[json_key] is mapping or (json_dict[json_key] is sequence and json_dict[json_key] is not string) %}
|
||||||
|
{{- '\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | tojson | safe) ~ '</' ~ json_key ~ '>' }}
|
||||||
|
{%- else %}
|
||||||
|
{{-'\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | string) ~ '</' ~ json_key ~ '>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
{%- set enable_thinking = enable_thinking if enable_thinking is defined else True %}
|
||||||
|
{%- set truncate_history_thinking = truncate_history_thinking if truncate_history_thinking is defined else True %}
|
||||||
|
|
||||||
|
{%- set ns = namespace(last_user_idx = -1) %}
|
||||||
|
{%- set loop_messages = messages %}
|
||||||
|
{%- for m in loop_messages %}
|
||||||
|
{%- if m["role"] == "user" %}
|
||||||
|
{%- set ns.last_user_idx = loop.index0 %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{%- if messages[0]["role"] == "system" %}
|
||||||
|
{%- set system_message = messages[0]["content"] %}
|
||||||
|
{%- set loop_messages = messages[1:] %}
|
||||||
|
{%- else %}
|
||||||
|
{%- set system_message = "" %}
|
||||||
|
{%- set loop_messages = messages %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if not tools is defined %}
|
||||||
|
{%- set tools = [] %}
|
||||||
|
{%- endif %}
|
||||||
|
{# Recompute last_user_idx relative to loop_messages after handling system #}
|
||||||
|
{%- set ns = namespace(last_user_idx = -1) %}
|
||||||
|
{%- for m in loop_messages %}
|
||||||
|
{%- if m["role"] == "user" %}
|
||||||
|
{%- set ns.last_user_idx = loop.index0 %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- if system_message is defined %}
|
||||||
|
{{- "<|im_start|>system\n" + system_message }}
|
||||||
|
{%- else %}
|
||||||
|
{%- if tools is iterable and tools | length > 0 %}
|
||||||
|
{{- "<|im_start|>system\n" }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if tools is iterable and tools | length > 0 %}
|
||||||
|
{%- if system_message is defined and system_message | length > 0 %}
|
||||||
|
{{- "\n\n" }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- "# Tools\n\nYou have access to the following functions:\n\n" }}
|
||||||
|
{{- "<tools>" }}
|
||||||
|
{%- for tool in tools %}
|
||||||
|
{%- if tool.function is defined %}
|
||||||
|
{%- set tool = tool.function %}
|
||||||
|
{%- endif %}
|
||||||
|
{{- "\n<function>\n<name>" ~ tool.name ~ "</name>" }}
|
||||||
|
{%- if tool.description is defined %}
|
||||||
|
{{- '\n<description>' ~ (tool.description | trim) ~ '</description>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '\n<parameters>' }}
|
||||||
|
{%- if tool.parameters is defined and tool.parameters is mapping and tool.parameters.properties is defined and tool.parameters.properties is mapping %}
|
||||||
|
{%- for param_name, param_fields in tool.parameters.properties|items %}
|
||||||
|
{{- '\n<parameter>' }}
|
||||||
|
{{- '\n<name>' ~ param_name ~ '</name>' }}
|
||||||
|
{%- if param_fields.type is defined %}
|
||||||
|
{{- '\n<type>' ~ (param_fields.type | string) ~ '</type>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if param_fields.description is defined %}
|
||||||
|
{{- '\n<description>' ~ (param_fields.description | trim) ~ '</description>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if param_fields.enum is defined %}
|
||||||
|
{{- '\n<enum>' ~ (param_fields.enum | tojson | safe) ~ '</enum>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- set handled_keys = ['name', 'type', 'description', 'enum'] %}
|
||||||
|
{{- render_extra_keys(param_fields, handled_keys) }}
|
||||||
|
{{- '\n</parameter>' }}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{% set handled_keys = ['type', 'properties', 'required'] %}
|
||||||
|
{{- render_extra_keys(tool.parameters, handled_keys) }}
|
||||||
|
{%- if tool.parameters is defined and tool.parameters.required is defined %}
|
||||||
|
{{- '\n<required>' ~ (tool.parameters.required | tojson | safe) ~ '</required>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '\n</parameters>' }}
|
||||||
|
{%- set handled_keys = ['type', 'name', 'description', 'parameters'] %}
|
||||||
|
{{- render_extra_keys(tool, handled_keys) }}
|
||||||
|
{{- '\n</function>' }}
|
||||||
|
{%- endfor %}
|
||||||
|
{{- "\n</tools>" }}
|
||||||
|
|
||||||
|
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{%- if system_message is defined %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- else %}
|
||||||
|
{%- if tools is iterable and tools | length > 0 %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- for message in loop_messages %}
|
||||||
|
{%- if message.role == "assistant" %}
|
||||||
|
{# Add reasoning content in to content field for unified processing below. #}
|
||||||
|
{%- if message.reasoning_content is defined and message.reasoning_content is string and message.reasoning_content | trim | length > 0 %}
|
||||||
|
{%- set content = "<think>\n" ~ message.reasoning_content ~ "\n</think>\n" ~ (message.content | default('', true)) %}
|
||||||
|
{%- else %}
|
||||||
|
{%- set content = message.content | default('', true) %}
|
||||||
|
{%- if content is string -%}
|
||||||
|
{# Allow downstream logic to to take care of broken thought, only handle coherent reasoning here. #}
|
||||||
|
{%- if '<think>' not in content and '</think>' not in content -%}
|
||||||
|
{%- set content = "<think></think>" ~ content -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- else -%}
|
||||||
|
{%- set content = content -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if message.tool_calls is defined and message.tool_calls is iterable and message.tool_calls | length > 0 %}
|
||||||
|
{# Assistant message has tool calls. #}
|
||||||
|
{{- '<|im_start|>assistant\n' }}
|
||||||
|
{%- set include_content = not (truncate_history_thinking and loop.index0 < ns.last_user_idx) %}
|
||||||
|
{%- if content is string and content | trim | length > 0 %}
|
||||||
|
{%- if include_content %}
|
||||||
|
{{- (content | trim) ~ '\n' -}}
|
||||||
|
{%- else %}
|
||||||
|
{%- set c = (content | string) %}
|
||||||
|
{%- if '</think>' in c %}
|
||||||
|
{# Keep only content after the last closing think. Also generation prompt causes this. #}
|
||||||
|
{%- set c = c.split('</think>')[-1] %}
|
||||||
|
{%- elif '<think>' in c %}
|
||||||
|
{# If <think> was opened but never closed, drop the trailing think segment #}
|
||||||
|
{%- set c = c.split('<think>')[0] %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- set c = "<think></think>" ~ c | trim %}
|
||||||
|
{%- if c | length > 0 %}
|
||||||
|
{{- c ~ '\n' -}}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- else %}
|
||||||
|
{{- "<think></think>" -}}
|
||||||
|
{%- endif %}
|
||||||
|
{%- for tool_call in message.tool_calls %}
|
||||||
|
{%- if tool_call.function is defined %}
|
||||||
|
{%- set tool_call = tool_call.function %}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '<tool_call>\n<function=' ~ tool_call.name ~ '>\n' -}}
|
||||||
|
{%- if tool_call.arguments is defined %}
|
||||||
|
{%- for args_name, args_value in tool_call.arguments|items %}
|
||||||
|
{{- '<parameter=' ~ args_name ~ '>\n' -}}
|
||||||
|
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
||||||
|
{{- args_value ~ '\n</parameter>\n' -}}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '</function>\n</tool_call>\n' -}}
|
||||||
|
{%- endfor %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- else %}
|
||||||
|
{# Assistant message doesn't have tool calls. #}
|
||||||
|
{%- if not (truncate_history_thinking and loop.index0 < ns.last_user_idx) %}
|
||||||
|
{{- '<|im_start|>assistant\n' ~ (content | default('', true) | string | trim) ~ '<|im_end|>\n' }}
|
||||||
|
{%- else %}
|
||||||
|
{%- set c = (content | default('', true) | string) %}
|
||||||
|
{%- if '<think>' in c and '</think>' in c %}
|
||||||
|
{%- set c = "<think></think>" ~ c.split('</think>')[-1] %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- set c = c | trim %}
|
||||||
|
{%- if c | length > 0 %}
|
||||||
|
{{- '<|im_start|>assistant\n' ~ c ~ '<|im_end|>\n' }}
|
||||||
|
{%- else %}
|
||||||
|
{{- '<|im_start|>assistant\n<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- elif message.role == "user" or message.role == "system" %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n' }}
|
||||||
|
{%- set content = message.content | string %}
|
||||||
|
{{- content }}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- elif message.role == "tool" %}
|
||||||
|
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
||||||
|
{{- '<|im_start|>user\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{{- '<tool_response>\n' }}
|
||||||
|
{{- message.content }}
|
||||||
|
{{- '\n</tool_response>\n' }}
|
||||||
|
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- elif loop.last %}
|
||||||
|
{{- '<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- else %}
|
||||||
|
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{%- if add_generation_prompt %}
|
||||||
|
{%- if enable_thinking %}
|
||||||
|
{{- '<|im_start|>assistant\n<think>\n' }}
|
||||||
|
{%- else %}
|
||||||
|
{{- '<|im_start|>assistant\n<think></think>' }}
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
|
|
@ -3588,6 +3588,163 @@ static void test_template_output_peg_parsers() {
|
||||||
t.expect.content =R"({"amount": 123.45, "date": "2025-12-03"})";
|
t.expect.content =R"({"amount": 123.45, "date": "2025-12-03"})";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// NVIDIA Nemotron-3 Nano
|
||||||
|
auto tmpls = read_templates("models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja");
|
||||||
|
|
||||||
|
// Test basic message
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input = "Hello, world!\nWhat's up?";
|
||||||
|
t.expect = message_assist;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test basic message and reasoning with reasoning_format = none
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input = "I'm\nthinking\n</think>\nHello, world!\nWhat's up?";
|
||||||
|
t.expect.content = "I'm\nthinking\n</think>\nHello, world!\nWhat's up?";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test basic message and reasoning with reasoning_format = auto
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input = "I'm\nthinking\n</think>\nHello, world!\nWhat's up?";
|
||||||
|
t.params.enable_thinking = true;
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
|
||||||
|
t.expect = message_assist_thoughts;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test tool call
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=special_function>\n"
|
||||||
|
"<parameter=arg1>\n"
|
||||||
|
"1\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>";
|
||||||
|
t.params.enable_thinking = false;
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.tools = {special_function_tool};
|
||||||
|
|
||||||
|
t.expect = message_assist_call;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test tool call with reasoning
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"I'm\nthinking\n</think>\n"
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=special_function>\n"
|
||||||
|
"<parameter=arg1>\n"
|
||||||
|
"1\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>";
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.tools = {special_function_tool};
|
||||||
|
|
||||||
|
t.expect = message_assist_call_thoughts;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test parallel tool calls
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=special_function>\n"
|
||||||
|
"<parameter=arg1>\n"
|
||||||
|
"1\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>\n"
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=special_function_with_opt>\n"
|
||||||
|
"<parameter=arg1>\n"
|
||||||
|
"1\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"<parameter=arg2>\n"
|
||||||
|
"2\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>";
|
||||||
|
t.params.enable_thinking = false;
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.parallel_tool_calls = true;
|
||||||
|
t.params.tools = {special_function_tool, special_function_tool_with_optional_param};
|
||||||
|
|
||||||
|
t.expect.tool_calls = {{
|
||||||
|
/* .name = */ "special_function",
|
||||||
|
/* .arguments = */ R"({"arg1": 1})",
|
||||||
|
/* .id = */ {},
|
||||||
|
}, {
|
||||||
|
/* .name = */ "special_function_with_opt",
|
||||||
|
/* .arguments = */ R"({"arg1": 1, "arg2": 2})",
|
||||||
|
/* .id = */ {},
|
||||||
|
}};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test tool call with string parameter
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=python>\n"
|
||||||
|
"<parameter=code>\n"
|
||||||
|
"def hello():\n"
|
||||||
|
" print(\"Hello, world!\")\n"
|
||||||
|
"\n"
|
||||||
|
"hello()\n"
|
||||||
|
"</parameter>\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>";
|
||||||
|
t.params.enable_thinking = false;
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.tools = {python_tool};
|
||||||
|
|
||||||
|
t.expect.tool_calls = {{
|
||||||
|
/* .name = */ "python",
|
||||||
|
/* .arguments = */ "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}",
|
||||||
|
/* .id = */ {},
|
||||||
|
}};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test tool call with string parameter and no closing </parameter> tag
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"<tool_call>\n"
|
||||||
|
"<function=python>\n"
|
||||||
|
"<parameter=code>\n"
|
||||||
|
"def hello():\n"
|
||||||
|
" print(\"Hello, world!\")\n"
|
||||||
|
"\n"
|
||||||
|
"hello()\n"
|
||||||
|
"</function>\n"
|
||||||
|
"</tool_call>";
|
||||||
|
t.params.enable_thinking = false;
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.tools = {python_tool};
|
||||||
|
|
||||||
|
t.expect.tool_calls = {{
|
||||||
|
/* .name = */ "python",
|
||||||
|
/* .arguments = */ "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}",
|
||||||
|
/* .id = */ {},
|
||||||
|
}};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test response format
|
||||||
|
test_peg_parser(tmpls.get(), [&](auto & t) {
|
||||||
|
t.input =
|
||||||
|
"I need to output the invoice details in JSON\n"
|
||||||
|
"</think>\n"
|
||||||
|
R"({"amount": 123.45, "date": "2025-12-03"})";
|
||||||
|
t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
|
||||||
|
t.params.json_schema = invoice_schema;
|
||||||
|
|
||||||
|
t.expect.reasoning_content = "I need to output the invoice details in JSON";
|
||||||
|
t.expect.content = R"({"amount": 123.45, "date": "2025-12-03"})";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_msg_diffs_compute() {
|
static void test_msg_diffs_compute() {
|
||||||
|
|
|
||||||
|
|
@ -1367,10 +1367,85 @@ static void test_all(const std::string & lang, std::function<void(const TestCase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_resolves_to_string() {
|
||||||
|
fprintf(stderr, "#\n# Testing resolves_to_string\n#\n");
|
||||||
|
|
||||||
|
auto test = [](const std::string & name, const std::string & schema_str, bool expected) {
|
||||||
|
fprintf(stderr, "- %s\n", name.c_str());
|
||||||
|
common_schema_info info;
|
||||||
|
auto schema = nlohmann::ordered_json::parse(schema_str);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Type array (nullable string)
|
||||||
|
test("type array with string", R"({"type": ["string", "null"]})", true);
|
||||||
|
test("type array without string", R"({"type": ["integer", "null"]})", false);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
fprintf(stderr, "LLAMA_NODE_AVAILABLE = %s\n", getenv("LLAMA_NODE_AVAILABLE") ? "true" : "false");
|
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");
|
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("C++", [](const TestCase & tc) {
|
||||||
try {
|
try {
|
||||||
tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true));
|
tc.verify(json_schema_to_grammar(nlohmann::ordered_json::parse(tc.schema), true));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue