1+ #include " chat-auto-parser-helpers.h"
12#include " chat-auto-parser.h"
23#include " chat-peg-parser.h"
34#include " chat.h"
@@ -23,31 +24,30 @@ static void foreach_function(const json & tools, const std::function<void(const
2324
2425namespace autoparser {
2526
26- parser_build_context::parser_build_context (common_chat_peg_builder & p, const templates_params & inputs) :
27+ parser_build_context::parser_build_context (common_chat_peg_builder & p, const generation_params & inputs) :
2728 p (p),
2829 inputs (inputs),
2930 reasoning_parser (p.eps()) {}
3031
3132common_chat_params peg_generator::generate_parser (const common_chat_template & tmpl,
32- const struct templates_params & inputs) {
33+ const struct generation_params & inputs) {
3334 // Run differential analysis to extract template structure
3435 struct autoparser autoparser;
3536 autoparser.analyze_template (tmpl);
3637 return generate_parser (tmpl, inputs, autoparser);
3738}
3839
3940common_chat_params peg_generator::generate_parser (const common_chat_template & tmpl,
40- const struct templates_params & inputs,
41+ const struct generation_params & inputs,
4142 const autoparser & autoparser) {
42- // Build the parser using the analysis results
43- auto parser = autoparser.build_parser (inputs);
44-
4543 // Create the result structure
4644 common_chat_params data;
4745 data.prompt = common_chat_template_direct_apply (tmpl, inputs);
4846 data.format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
4947 data.preserved_tokens = autoparser.preserved_tokens ;
50- data.parser = parser.save ();
48+
49+ auto parser = autoparser.build_parser (inputs);
50+ data.parser = parser.save ();
5151
5252 // Build grammar if tools are present
5353 bool has_tools =
@@ -82,44 +82,38 @@ common_chat_params peg_generator::generate_parser(const common_chat_template &
8282 return data;
8383}
8484
85- common_peg_arena autoparser::build_parser (const templates_params & inputs) const {
85+ common_peg_arena autoparser::build_parser (const generation_params & inputs) const {
8686 if (!analysis_complete) {
8787 throw std::invalid_argument (" Cannot call build_parser on autoparser without performing analysis first, call analyze_template(...)" );
8888 }
8989 return build_chat_peg_parser ([&](common_chat_peg_builder & p) {
90- // If the template uses Python dict format (single-quoted strings in JSON structures),
91- // pre-register a json-string rule that accepts both quote styles. This must happen
92- // before any call to p.json() so that all JSON parsing inherits the flexible rule.
93- if (tools.format .uses_python_dicts ) {
94- p.rule (" json-string" , p.quoted_string ());
95- }
96-
9790 parser_build_context ctx (p, inputs);
9891 bool extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE ;
99- bool enable_thinking = inputs.enable_thinking ;
10092
101- ctx.extracting_reasoning = extract_reasoning && enable_thinking && reasoning.mode != reasoning_mode::NONE ;
93+ ctx.extracting_reasoning = extract_reasoning && reasoning.mode != reasoning_mode::NONE ;
10294 ctx.content = &content;
10395
10496 // Build reasoning parser
10597 ctx.reasoning_parser = reasoning.build_parser (ctx);
10698
99+ auto parser = p.eps ();
100+
107101 bool has_tools = inputs.tools .is_array () && !inputs.tools .empty ();
108102 bool has_response_format = inputs.json_schema .is_object () && !inputs.json_schema .empty ();
109103
110104 if (has_response_format) {
111105 auto response_format = p.rule (" response-format" , p.content (p.schema (p.json (), " response-format-schema" , inputs.json_schema )));
112- return ctx.reasoning_parser + p.space () + p.choice ({
106+ parser = ctx.reasoning_parser + p.space () + p.choice ({
113107 p.literal (" ```json" ) + p.space () + response_format + p.space () + p.literal (" ```" ),
114108 response_format
115109 }) + p.end ();
110+ } else if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && jinja_caps.supports_tool_calls ) {
111+ parser = tools.build_parser (ctx);
112+ } else {
113+ parser = content.build_parser (ctx);
116114 }
117-
118- if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && jinja_caps.supports_tool_calls ) {
119- return tools.build_parser (ctx);
120- }
121-
122- return content.build_parser (ctx);
115+ parser = wrap_for_generation_prompt (p, parser, inputs, reasoning.start );
116+ return parser;
123117 });
124118}
125119
@@ -130,24 +124,15 @@ common_peg_parser analyze_reasoning::build_parser(parser_build_context & ctx) co
130124 return p.eps ();
131125 }
132126
133- bool thinking_forced_open = (mode == reasoning_mode::FORCED_OPEN );
134- bool thinking_forced_closed = (mode == reasoning_mode::FORCED_CLOSED );
135-
136- if (thinking_forced_open || thinking_forced_closed) {
137- // Thinking is forced open OR forced closed with enable_thinking=true
138- // In both cases, expect only the closing tag (opening was in template)
139- // However, since we might have incorrectly detected the open/close pattern,
140- // we admit an optional starting marker
141- return p.optional (p.literal (start)) + p.reasoning (p.until (end)) + end;
142- }
143127 if (mode == reasoning_mode::TAG_BASED || mode == reasoning_mode::TOOLS_ONLY ) {
144- // Standard tag-based reasoning OR tools-only mode (reasoning appears with tools)
145- // Both use the same tag-based pattern if markers are available
146- if (!start.empty () && !end.empty ()) {
147- return p.optional (start + p.reasoning (p.until (end)) + end);
128+ if (!end.empty ()) {
129+ if (!start.empty ()) {
130+ // Standard tag-based: optional(<think>reasoning</think>)
131+ return p.optional (start + p.reasoning (p.until (end)) + end + p.space ());
132+ }
133+ // Delimiter-style (empty start)
134+ return p.optional (p.reasoning (p.until (end)) + end + p.space ());
148135 }
149- } else if (mode == reasoning_mode::DELIMITER ) {
150- return p.optional (p.reasoning (p.until (end)) + end);
151136 }
152137
153138 return p.eps ();
@@ -335,7 +320,7 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
335320 " tool-" + name + " -arg-" + param_name + " -schema" ,
336321 param_schema, true )) :
337322 p.tool_arg_json_value (p.schema (
338- p.json (), " tool-" + name + " -arg-" + param_name + " -schema" , param_schema, format. uses_python_dicts )) +
323+ p.json (), " tool-" + name + " -arg-" + param_name + " -schema" , param_schema, false )) +
339324 p.space ()) +
340325 p.tool_arg_close (p.literal (arguments.value_suffix )));
341326
@@ -384,7 +369,9 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
384369 func_parser = p.atomic (p.tool_open (function.name_prefix + p.tool_name (p.literal (name)) + function.name_suffix ) +
385370 call_id_section) + p.space () + args_seq;
386371 matched_atomic = true ;
387- } else if (!arguments.name_prefix .empty () && properties.size () > 0 ) {
372+ } else if (!arguments.name_prefix .empty () && !required_parsers.empty ()) {
373+ // Only peek for an arg tag when there are required args that must follow.
374+ // When all args are optional, the model may emit no arg tags at all (#20650).
388375 func_parser = p.atomic (p.tool_open (function.name_prefix + p.tool_name (p.literal (name)) + function.name_suffix ) +
389376 call_id_section + p.space () + p.peek (p.literal (arguments.name_prefix ))) + args_seq;
390377 matched_atomic = true ;
0 commit comments