Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions common/chat-auto-parser-generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ common_chat_params peg_generator::generate_parser(const common_chat_template &
data.grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_marker }
};
if (autoparser.tools.format.openai_wrapper_trigger) {
// model emits the OpenAI function wrapper, trigger on it
data.grammar_triggers.push_back({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "{\"type\": \"function\"," });
}
}
}

Expand Down Expand Up @@ -224,13 +228,13 @@ common_peg_parser analyze_tools::build_tool_parser_json_native(parser_build_cont
auto single_tool_parser = p.standard_json_tools(
format.per_call_start, format.per_call_end, inputs.tools, inputs.parallel_tool_calls,
inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED, name_field, args_field, format.tools_array_wrapped,
format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order);
format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order, format.openai_wrapper_trigger);
tools_parser = p.trigger_rule("tool-calls", p.one_or_more(single_tool_parser + p.space()));
} else {
tools_parser = p.standard_json_tools(
format.section_start, format.section_end, inputs.tools, inputs.parallel_tool_calls,
inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED, name_field, args_field, format.tools_array_wrapped,
format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order);
format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order, format.openai_wrapper_trigger);
}

// Handle content wrappers if present
Expand Down
1 change: 1 addition & 0 deletions common/chat-auto-parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ struct tool_format_analysis {

bool fun_name_is_key = false; // In JSON format function name is JSON key, i.e. { "<funname>": { ... arguments ... } }
bool tools_array_wrapped = false; // Tool calls wrapped in JSON array [...]
bool openai_wrapper_trigger = false; // model emits the OpenAI function wrapper, trigger on it

std::string function_field = "function";
std::string name_field = "name";
Expand Down
8 changes: 8 additions & 0 deletions common/chat-diff-analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ static std::vector<std::function<void(const common_chat_template & tmpl, autopar
LOG_DBG(ANSI_ORANGE "[Patch: Apriel 1.6]\n" ANSI_RESET);
}
},
// template uses the JSON {name, parameters} tool instruction, emits the OpenAI function wrapper
[](const common_chat_template & tmpl, autoparser & analysis) -> void {
if (tmpl.src.find("Respond in the format {\"name\": function name") != std::string::npos &&
tmpl.src.find("Do not use variables.") != std::string::npos) {
analysis.tools.format.openai_wrapper_trigger = true;
LOG_DBG(ANSI_ORANGE "[Patch: JSON name/parameters tool instruction]\n" ANSI_RESET);
}
},

});

Expand Down
16 changes: 12 additions & 4 deletions common/chat-peg-parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,8 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys(
const std::string & effective_args_key,
const std::string & call_id_key,
const std::string & gen_call_id_key,
const std::vector<std::string> & parameters_order) {
const std::vector<std::string> & parameters_order,
bool accept_openai_wrapper) {

auto tool_choices = choice();
auto name_key_parser = literal("\"" + effective_name_key + "\"");
Expand Down Expand Up @@ -807,7 +808,13 @@ common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys(
return idx_a < idx_b;
});

auto ordered_body = tool_open(literal("{")) + space();
// accept an optional leading "type": "function" field when the model emits the OpenAI wrapper
common_peg_parser type_field = eps();
if (accept_openai_wrapper) {
type_field = optional(literal("\"type\"") + space() + literal(":") + space() +
literal("\"function\"") + space() + literal(",") + space());
}
auto ordered_body = tool_open(literal("{")) + space() + type_field;
for (size_t i = 0; i < parser_pairs.size(); i++) {
ordered_body = ordered_body + parser_pairs[i].first;
if (i < parser_pairs.size() - 1) {
Expand Down Expand Up @@ -870,7 +877,8 @@ common_peg_parser common_chat_peg_builder::standard_json_tools(
bool function_is_key,
const std::string & call_id_key,
const std::string & gen_call_id_key,
const std::vector<std::string> & parameters_order) {
const std::vector<std::string> & parameters_order,
bool accept_openai_wrapper) {
if (!tools.is_array() || tools.empty()) {
return eps();
}
Expand All @@ -888,7 +896,7 @@ common_peg_parser common_chat_peg_builder::standard_json_tools(
if (!name_spec.first.empty() || !args_spec.first.empty()) {
tool_choices = build_json_tools_nested_keys(tools, effective_name_key, effective_args_key, call_id_key, gen_call_id_key);
} else {
tool_choices = build_json_tools_flat_keys(tools, effective_name_key, effective_args_key, call_id_key, gen_call_id_key, parameters_order);
tool_choices = build_json_tools_flat_keys(tools, effective_name_key, effective_args_key, call_id_key, gen_call_id_key, parameters_order, accept_openai_wrapper);
}
}

Expand Down
6 changes: 4 additions & 2 deletions common/chat-peg-parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ class common_chat_peg_builder : public common_peg_parser_builder {
bool function_is_key = false,
const std::string & call_id_key = "",
const std::string & gen_call_id_key = "",
const std::vector<std::string> & parameters_order = {});
const std::vector<std::string> & parameters_order = {},
bool accept_openai_wrapper = false);

// Legacy-compatible helper for building XML/tagged style tool calls
// Used by tests and manual parsers
Expand Down Expand Up @@ -157,7 +158,8 @@ class common_chat_peg_builder : public common_peg_parser_builder {
const std::string & effective_args_key,
const std::string & call_id_key,
const std::string & gen_call_id_key,
const std::vector<std::string> & parameters_order);
const std::vector<std::string> & parameters_order,
bool accept_openai_wrapper);
};

inline common_peg_arena build_chat_peg_parser(
Expand Down
6 changes: 4 additions & 2 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2520,8 +2520,10 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
}
return msg;
}
throw std::runtime_error(std::string("Failed to parse input at pos ") + std::to_string(result.end) + ": " +
effective_input.substr(result.end));
LOG_WRN("%s: unparsed %s output: %s\n", __func__, common_chat_format_name(params.format),
effective_input.substr(result.end).c_str());
throw std::runtime_error(std::string("The model produced output that does not match the expected ") +
common_chat_format_name(params.format) + " format");
}

common_chat_msg msg;
Expand Down
80 changes: 77 additions & 3 deletions common/json-schema-to-grammar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,29 @@ class common_schema_converter {
return _add_rule("dot", rule);
};

// Translates a PCRE-style shorthand character-class escape (\\d, \\D, \\w, \\W, \\s, \\S)
// that appears OUTSIDE of a [...] bracket expression into its own standalone GBNF
// character-class rule (analogous to get_dot() above for '.'). Unlike a shorthand escape
// mixed inside a [...] class, a standalone shorthand escape (positive or negated) always
// has a clean, exact GBNF translation.
auto get_shorthand_class = [&](char esc) -> std::string {
switch (esc) {
case 'd': return _add_rule("d", "[0-9]");
case 'D': return _add_rule("not-d", "[^0-9]");
case 'w': return _add_rule("w", "[A-Za-z0-9_]");
case 'W': return _add_rule("not-w", "[^A-Za-z0-9_]");
case 's': return _add_rule("s", "[ \\t\\n\\r]");
case 'S': return _add_rule("not-s", "[^ \\t\\n\\r]");
default:
// unreachable: only invoked for d/D/w/W/s/S, see dispatch below.
_errors.push_back("Unsupported shorthand escape '\\" + std::string(1, esc) + "' in pattern '" + pattern + "'");
return _add_rule("unknown-escape", "[]");
}
};
auto is_shorthand_class = [](char c) {
return c == 'd' || c == 'D' || c == 'w' || c == 'W' || c == 's' || c == 'S';
};

// Joins the sequence, merging consecutive literals together.
auto join_seq = [&]() {
std::vector<literal_or_rule> ret;
Expand Down Expand Up @@ -414,6 +437,12 @@ class common_schema_converter {
if (c == '.') {
seq.emplace_back(get_dot(), false);
i++;
} else if (c == '\\' && i + 1 < length && is_shorthand_class(sub_pattern[i + 1])) {
// Standalone \\d, \\D, \\w, \\W, \\s, \\S (i.e. not nested inside a [...] class):
// translate to their own GBNF character-class rule so this token composes with
// quantifiers (*, +, ?, {m,n}) exactly like get_dot()'s "." handling above.
seq.emplace_back(get_shorthand_class(sub_pattern[i + 1]), false);
i += 2;
} else if (c == '(') {
i++;
if (i < length && sub_pattern[i] == '?') {
Expand Down Expand Up @@ -447,9 +476,47 @@ class common_schema_converter {
std::string square_brackets = std::string(1, c);
i++;
while (i < length && sub_pattern[i] != ']') {
if (sub_pattern[i] == '\\') {
square_brackets += sub_pattern.substr(i, 2);
i += 2;
if (sub_pattern[i] == '\\' && i + 1 < length) {
char esc = sub_pattern[i + 1];
// PCRE shorthand classes have no GBNF escape of their own (GBNF's [...] only
// understands literal chars, ranges, and its own \\x/\\u/\\U/\\t/\\r/\\n/\\\\/\\"/\\[/\\]
// escapes) -- src/llama-grammar.cpp's parse_char() throws "unknown escape" on
// anything else. \\d/\\w/\\s are positive classes, so they can always be
// inlined as extra members of this (possibly mixed) [...] class. \\D/\\W/\\S
// are negated classes: inlining them alongside other members of a positive
// class has no single-range GBNF equivalent (e.g. [\\D2468] can't be expressed
// as one flat GBNF character class), so fail loudly here at schema-conversion
// time -- with the offending pattern named -- rather than emitting grammar text
// that will only fail later, without context, inside the GBNF parser.
switch (esc) {
case 'd':
square_brackets += "0-9";
i += 2;
break;
case 'w':
square_brackets += "A-Za-z0-9_";
i += 2;
break;
case 's':
square_brackets += " \\t\\n\\r";
i += 2;
break;
case 'D':
case 'W':
case 'S':
_errors.push_back(
"Pattern '" + pattern + "': negated shorthand class '\\" + std::string(1, esc) +
"' is not supported inside a [...] character class (GBNF has no single-range "
"equivalent for a negated shorthand mixed with other class members); rewrite "
"the pattern to use '\\" + std::string(1, esc) + "' on its own outside of "
"brackets, or replace it with an explicit negated range, e.g. [^0-9] for \\D");
i += 2;
break;
default:
square_brackets += sub_pattern.substr(i, 2);
i += 2;
break;
}
} else {
square_brackets += sub_pattern[i];
i++;
Expand Down Expand Up @@ -525,6 +592,13 @@ class common_schema_converter {
while (i < length) {
if (sub_pattern[i] == '\\' && i < length - 1) {
char next = sub_pattern[i + 1];
if (is_shorthand_class(next)) {
// Don't swallow \d/\D/\w/\W/\s/\S into the literal run: break out
// (flushing whatever literal text was collected so far) and let the
// outer dispatch loop's shorthand-class branch above handle it as its
// own token, the same way it would if it started the token.
break;
}
if (ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.find(next) != ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS.end()) {
i++;
literal += sub_pattern[i];
Expand Down
3 changes: 3 additions & 0 deletions common/sampling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, st
}
}
}
if (!grmr && !grammar_str.empty()) {
throw std::runtime_error("failed to parse grammar");
}

// Compute prefill tokens from the generation prompt
std::vector<llama_token> prefill_tokens;
Expand Down
Loading