char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
3ad1ba733
char/aliceai-t5-moe
Auto-Parser Architecture
The auto-parser automatically analyzes chat templates to determine how to parse model outputs, including content, reasoning, and tool calls.
Overview
The unified auto-parser uses a pure differential, compositional approach (inspired by the git diff algorithm) to analyze chat templates:
Core Philosophy:
- Minimize Hardcoded Patterns: All markers extracted through template comparison (the only heuristic is JSON detection to distinguish
JSON_NATIVEfrom tag-based formats) - Compositional Architecture: Separate analyzer structs for reasoning, content, and tools — each responsible for its own analysis and parser construction
Analysis + Parser Building in Two Steps:
autoparser::autoparser tmpl_analysis(tmpl)— runs all differential comparisons and populates the analysis structsautoparser::peg_generator::generate_parser(tmpl, generation_params, tmpl_analysis)— uses the analysis to build a PEG parser and optional GBNF grammar
Data Structures
All structs are defined in common/chat-auto-parser.h.
Top-Level: autoparser (main analyzer and generator)
common/chat-auto-parser.h:367-388 — top-level analysis result aggregating jinja_caps, reasoning, content, and tools sub-analyses, plus preserved_tokens (union of all non-empty markers).
analyze_reasoning
common/chat-auto-parser.h:254-274 — reasoning analysis result: mode enum, start marker (e.g. <think>), and end marker (e.g. </think>).
analyze_content
common/chat-auto-parser.h:280-295 — content analysis result: mode enum, start/end markers, and requires_nonnull_content flag.
analyze_tools and its sub-structs
- common/chat-auto-parser.h:176-194 —
tool_format_analysis:modeenum,section_start/end,per_call_start/end, JSON field names (function_field,name_field,args_field,id_field,gen_id_field), and format flags (fun_name_is_key,tools_array_wrapped) - common/chat-auto-parser.h:196-200 —
tool_function_analysis:name_prefix,name_suffix,closemarkers around function names - common/chat-auto-parser.h:202-210 —
tool_arguments_analysis:start/endcontainer markers,name_prefix/suffix,value_prefix/suffix,separator - common/chat-auto-parser.h:212-217 —
tool_id_analysis:posenum,prefix/suffixmarkers around call ID values - common/chat-auto-parser.h:301-361 —
analyze_tools: aggregates the four sub-structs above
Enums
reasoning_mode: How the template handles reasoning/thinking blocks.
| Value | Description |
|---|---|
NONE | No reasoning markers detected |
TAG_BASED | Tag-based: <think>...</think> (start can be empty for delimiter-style formats) |
TOOLS_ONLY | Reasoning only appears in tool call responses, not plain content |
Generation Prompt & Reasoning Prefill: Computed in common_chat_templates_apply_jinja before invoking either the specialized handlers or the auto-parser, by rendering the template twice — once with add_generation_prompt=false and once with add_generation_prompt=true — and storing the diff suffix as generation_params::generation_prompt. This string is propagated into common_chat_params::generation_prompt and common_chat_parser_params::generation_prompt.
The generation prompt is prepended to model output before PEG parsing via wrap_for_generation_prompt(). The portion before the reasoning start marker (if any) is prepended as a literal to ensure any boilerplate added by the template is consumed. The full string is also fed to the grammar sampler via llama_sampler_accept (stored in common_params_sampling::grammar_prefill), advancing the grammar past tokens already in the prompt. It is used to determine the reasoning budget sampler's initial state — COUNTING if the prefill tokens begin with the reasoning start sequence (but don't also contain the end sequence), IDLE otherwise.
grammar_prefill (common_params_sampling): The generation prompt string tokenized and accepted by the grammar sampler at init time. Only applied when grammar_external is false (i.e., the grammar was not set explicitly by the user).
Three outcomes for reasoning-prefill handling (in generate_parser()):
- Start+end in generation prompt (e.g.
<think></think>\n): the parser sees reasoning as opened and immediately closed; whitespace-only reasoning content is discarded. - Only start in generation prompt (e.g.
<think>\n): the parser sees reasoning as already open. - Start marker present but not at the end (e.g. Apriel's
<|begin_assistant|>followed by boilerplate): the marker is a template artifact; the start literal is cleared so reasoning uses delimiter-style (end-only). For templates that ignoreadd_generation_prompt(empty diff), the rendereddata.promptis used as fallback — but only for non-TOOLS_ONLY modes, since in TOOLS_ONLY the start tag is model-generated and may appear in prior conversation turns.
content_mode: How the template wraps assistant content.
| Value | Description |
|---|---|
PLAIN | No content markers |
ALWAYS_WRAPPED | Content always wrapped: <response>...</response> |
WRAPPED_WITH_REASONING | Content wrapped only when reasoning is present |
tool_format: Classification of tool call structure.
| Value | Description |
|---|---|
NONE | No tool support detected |
JSON_NATIVE | Pure JSON: {"name": "X", "arguments": {...}} |
TAG_WITH_JSON | Tag-based with JSON args: <function=X>{...}</function> |
TAG_WITH_TAGGED | Tag-based with tagged args: <param=key>value</param> |
call_id_position: Where call IDs appear in tag-based formats.
| Value | Description |
|---|---|
NONE | No call ID support detected |
PRE_FUNC_NAME | Before function name |
BETWEEN_FUNC_AND_ARGS | Between function name and arguments |
POST_ARGS | After arguments |
Tool Calling Formats
JSON_NATIVE
Structure: The entire tool call (function name, arguments, values) is in JSON format. Optional enclosing tags around the section.
Detection: Function name appears inside a JSON structure (quotes preceded by { or :).
Examples:
Standard OpenAI-style:
<tool_call>
{"name" : "get_weather" , "arguments" : {"location" : "Paris" , "unit" : "celsius" }}
</tool_call>
Mistral Nemo with array wrapper:
[TOOL_CALLS]
[{"name" : "calculate" , "arguments" : {"expr" : "2+2" }}]
Function name as JSON key (Apertus style):
{"get_weather" : {"location" : "Paris" }}
TAG_WITH_JSON
Structure: Function name is outside JSON, in tag attributes or XML-style tags. Arguments are a JSON object.
Detection: Function name not in JSON, but argument names appear in JSON context.
Examples:
Functionary v3.1:
< function = get_weather > {"location": "Paris", "unit": "celsius"}</ function >
MiniMax:
< minimax:tool_call > < tool_name > calculate</ tool_name > < arguments > {"expr": "2+2"}</ arguments > </ minimax:tool_call >
TAG_WITH_TAGGED
Structure: Both function name and argument names are in XML-style tags. String values are unquoted; non-string values are JSON-formatted.
Detection: Neither function name nor argument names appear in a JSON context.
Examples:
Qwen/Hermes XML format:
< function = get_weather > < param = location > Paris</ param > < param = unit > celsius</ param > </ function >
Mixed types:
< function = calculate > < param = expr > 2+2</ param > < param = precision > 2</ param > < param = options > {"round": true}</ param > </ function >
String values (Paris, celsius, 2+2) are unquoted; options (object type) is JSON-formatted.
Analysis Flow
autoparser::autoparser(tmpl)
|
|-- Phase 1: analyze_reasoning(tmpl, jinja_caps.supports_tool_calls)
| |-- R1: compare_reasoning_presence() — with/without reasoning_content field
| |-- R2: compare_thinking_enabled() — enable_thinking=false vs true
| '-- R3: compare_reasoning_scope() — reasoning+content vs reasoning+tools
| (only if supports_tool_calls)
|
|-- Phase 2: analyze_content(tmpl, reasoning)
| '-- C1: compares content-only vs tools output and content-only vs reasoning output
|
|-- Phase 3: analyze_tools(tmpl, jinja_caps, reasoning)
| (skipped entirely if !jinja_caps.supports_tool_calls)
| |
| |-- T1: analyze_tool_calls() — no tools vs with tools; classifies format
| | |-- JSON path → analyze_tool_call_format_json_native()
| | '-- tag path → analyze_tool_call_format_non_json()
| |
| (if format != NONE and format != JSON_NATIVE:)
| |
| |-- T2: check_per_call_markers() — 1 call vs 2 calls; moves section→per-call if needed
| | (only if supports_parallel_tool_calls)
| |
| |-- T3: extract_function_markers() — func_alpha vs func_beta; extracts name prefix/suffix/close
| |
| |-- T4: analyze_arguments() — (TAG_WITH_TAGGED only)
| | |-- A1: extract_argument_name_markers() — arg_name_A vs arg_name_B
| | '-- A2: extract_argument_value_markers() — value "XXXX" vs "YYYY"
| |
| |-- T5: extract_argument_separator() — 1 arg vs 2 args; finds separator between args
| |
| |-- T6: extract_args_markers() — 0 args vs 1 arg; finds args container markers
| |
| '-- T7: extract_call_id_markers() — call_id "call00001" vs "call99999"
|
'-- collect_preserved_tokens() — union of all non-empty markers
|
'-- apply workarounds() — post-hoc patches for edge-case templates
|
v
autoparser (analysis result)
|
v
autoparser::peg_generator::generate_parser(tmpl, inputs, analysis)
|-- analysis.build_parser(inputs) — builds PEG parser arena
| |-- reasoning.build_parser(ctx) — reasoning parser (mode-dependent)
| |-- content.build_parser(ctx) — content parser (mode-dependent)
| '-- tools.build_parser(ctx) — tool parser (dispatches by tool_format)
| |-- build_tool_parser_json_native()
| |-- build_tool_parser_tag_json()
| '-- build_tool_parser_tag_tagged()
|
|-- Build GBNF grammar (if tools present and trigger_marker non-empty)
'-- Set grammar_triggers from section_start or per_call_start
|
v
common_chat_params (prompt, parser, grammar, triggers, preserved_tokens)
Entry Point
The auto-parser is invoked in common/chat.cpp:1280-1310 in common_chat_templates_apply_jinja. A few specialized templates are handled first (Ministral/Magistral Large 3, GPT-OSS with <|channel|>, Functionary v3.2 with >>>all), then the auto-parser handles everything else via autoparser::autoparser + peg_generator::generate_parser.
Algorithm Details
Core Mechanism: Differential Comparison
All analysis phases use the same factorized comparison function declared in common/chat-auto-parser-helpers.h:68:
compare_variants (tmpl ,params_A ,params_modifier )
This creates variant B by applying a modifier lambda to a copy of params_A, renders both through the template, and computes a diff_split (common/chat-auto-parser.h:28-37):
prefix— common prefix between A and Bsuffix— common suffix between A and Bleft— unique to variant Aright— unique to variant B
The diff is computed via calculate_diff_split(), which finds the longest-common-prefix and longest-common-suffix, then iteratively moves incomplete <...> or [...] markers from the prefix/suffix into left/right until stable (tag boundary fixing).
Text is segmentized into markers and non-marker fragments using segmentize_markers(), which splits on <...> and [...] boundaries.
Phase 1: Reasoning Analysis
R1 — compare_reasoning_presence(): Compares assistant message with vs without a reasoning_content field.
- Searches
diff.right(output with reasoning) for the reasoning content needle - Uses PEG parsers to find surrounding markers:
- If both pre/post markers found in
diff.right→TAG_BASED - If both found but post marker only in the full output B →
TAG_BASED(template forces markers; handled via prefill) - If only post marker found →
TAG_BASED(delimiter-style, empty start)
- If both pre/post markers found in
- Sets
reasoning.startandreasoning.end
R2 — compare_thinking_enabled(): Compares enable_thinking=false vs true with a generation prompt.
- Detects template-added reasoning markers:
enable_thinking=trueappends a non-empty marker → setsreasoning.start, mode =TAG_BASED - Handles the reverse case (
enable_thinking=falseappends the marker instead): extracts both start (from the preceding segment) and end markers; mode =TAG_BASED - The reasoning prefill (markers added by the template) is later extracted in
common_chat_templates_apply_jinjaand prepended to model output before parsing
R3 — compare_reasoning_scope(): Compares assistant message with reasoning+text-content vs reasoning+tool-calls.
- Only runs if
jinja_caps.supports_tool_calls - Detects
TOOLS_ONLY: reasoning content present in B (with tools) but not in A (with text content) - Extracts reasoning markers from the tool call output using PEG parsers
Phase 2: Content Analysis
C1: Two comparisons in the analyze_content constructor:
- Comparison 1: content-only output vs tool-call output →
diff_tools - Comparison 2: content-only output vs reasoning+empty-content output →
diff_reasoning
Classification logic:
PLAIN:diff_tools.leftequals the response string (content is the entire diff, no wrapper)ALWAYS_WRAPPED: markers found surrounding the content text inpure_content→ extractsstart/end
Phase 3: Tool Call Analysis
T1 — analyze_tool_calls(): Compares no-tools vs with-tools output.
- Extracts the tool call section as
diff.right - Calls
analyze_tool_call_format()which first strips reasoning markers from the haystack, then:- Calls
in_json_haystack()for both function name and argument name needles in_json_haystack()uses a PEG parser to check whether the needle appears in a JSON context (preceded by{or:with surrounding quotes)- If function name is in JSON →
JSON_NATIVE→analyze_tool_call_format_json_native() - If function name not in JSON, arg name is in JSON →
TAG_WITH_JSON - If neither in JSON →
TAG_WITH_TAGGED analyze_tool_call_format_json_native(): parses the JSON object, matches field values to needles to populatename_field,args_field,id_field,gen_id_field; detectstools_array_wrapped; extractssection_start/section_endanalyze_tool_call_format_non_json(): uses PEG parsers on the haystack to find up to two opening markers (section + per-call) then up to two closing markers
- Calls
T2 — check_per_call_markers(): Compares 1 call vs 2 calls.
- Computes a secondary diff of the second call portion vs the common suffix
- If the second call content starts with
section_start→ the section marker is actually per-call → movessection_start/endtoper_call_start/endand clears the section markers
T3 — extract_function_markers(): Compares function name FUN_FIRST vs FUN_SECOND (two different named functions).
- Finds where the function name appears in
diff.left - Extracts
function.name_prefixfrom the common prefix up to the function marker, andfunction.name_suffixfrom after the name up to the next marker - Extends
name_suffixintodiff.suffix(to the first marker for TAG_WITH_TAGGED; to the first{or[for TAG_WITH_JSON) - Extracts
function.closefrom after the last argument value up to the per-call/section end marker
T4 — analyze_arguments() (TAG_WITH_TAGGED only):
- A1
extract_argument_name_markers(): Comparesarg_name_Avsarg_name_B(two different argument names).- Finds shared surrounding structure →
arguments.name_prefix,arguments.name_suffix
- Finds shared surrounding structure →
- A2
extract_argument_value_markers(): Compares argument value"XXXX"vs"YYYY"(same arg, different value).- Finds markers surrounding the value →
arguments.value_prefix,arguments.value_suffix
- Finds markers surrounding the value →
T5 — extract_argument_separator(): Compares 1 argument vs 2 arguments (same function).
- Uses
until_common_prefix(diff.right, ARG_FIRST, ARG_SECOND)to find what separates the two argument blocks
T6 — extract_args_markers(): Compares 0 arguments vs 1 argument.
- Uses
until_common_prefix()andafter_common_suffix()with the empty and single-arg JSON strings as anchors to find container markers (arguments.start,arguments.end)
T7 — extract_call_id_markers(): Compares call IDs "call00001" vs "call99999".
- Determines whether function name appears in
diff.prefixordiff.suffixto classify position:- Function name in prefix only →
BETWEEN_FUNC_AND_ARGSorPOST_ARGS(further distinguished by where{appears) - Function name in suffix only →
PRE_FUNC_NAME
- Function name in prefix only →
- Extracts
call_id.prefixandcall_id.suffixmarkers around the call ID value - Clears
per_call_endif it incorrectly incorporated the call ID suffix
Workarounds
A workaround array in common/chat-diff-analyzer.cpp applies post-hoc patches after analysis. Each workaround is a lambda that inspects the template source and overrides analysis results. Current workarounds:
- Old Qwen/DeepSeek thinking templates — source contains
content.split('</think>')but not<SPECIAL_12>: setsreasoning.mode = TAG_BASEDwith<think>/</think>markers if no reasoning was detected - Granite 3.3 — source contains specific "Write your thoughts" text: forces
TAG_BASEDreasoning with<think>/</think>andWRAPPED_WITH_REASONINGcontent with<response>/</response> - Cohere Command R+ — source contains
<|CHATBOT_TOKEN|>: setsALWAYS_WRAPPEDcontent mode if no content start is already set - Functionary 3.1 — source contains
set has_code_interpreter: forcesPLAINcontent, specificper_call_start/end, clears preserved tokens to only keep Functionary-specific markers - DeepSeek-R1-Distill-Qwen — source contains
tool▁calls▁beginmarkers: overrides tool section/per-call markers with the correct Unicode block characters
Parser Building
Each analyzer struct (analyze_reasoning, analyze_content, analyze_tools) implements build_parser(parser_build_context&). They share a parser_build_context that carries the PEG builder, inference inputs, the pre-built reasoning parser, and a pointer to the content analyzer.
Reasoning Parser (analyze_reasoning::build_parser)
| Mode | Parser |
|---|---|
| Not extracting reasoning | eps() |
TAG_BASED or TOOLS_ONLY (non-empty start) | optional(start + reasoning(until(end)) + end + space()) |
TAG_BASED or TOOLS_ONLY (empty start) | optional(reasoning(until(end)) + end + space()) — delimiter-style |
Note: The start marker may be empty either because the analyzer detected delimiter-style reasoning, or because generate_parser() cleared a template artifact start marker (see Generation Prompt & Reasoning Prefill above). Whitespace-only reasoning content (e.g. from a <think></think> prefill) is discarded by the mapper.
Content Parser (analyze_content::build_parser)
| Condition | Parser |
|---|---|
json_schema present | reasoning + space() + content(schema(json(), "response-format", ...)) + end() |
| Tools present | Dispatches to analyze_tools::build_parser() |
ALWAYS_WRAPPED with reasoning | reasoning + start + content(until(end)) + end + end() |
ALWAYS_WRAPPED without reasoning | content(until(start)) + start + content(until(end)) + end + end() |
| Default (PLAIN) | reasoning + content(rest()) + end() |
Tool Parsers (analyze_tools::build_parser)
Dispatches by format.mode:
build_tool_parser_json_native(): Calls p.standard_json_tools() which internally dispatches to:
build_json_tools_function_is_key()— function name is the JSON key:{"get_weather": {...}}build_json_tools_nested_keys()— nested:{"function": {"name": "X", "arguments": {...}}}build_json_tools_flat_keys()— flat:{"name": "X", "arguments": {...}}
Handles content wrappers, array wrapping (tools_array_wrapped), parallel calls, and parameter_order.
build_tool_parser_tag_json(): For each tool function:
tool_open(name_prefix + tool_name(literal(name)) + name_suffix) +
call_id_section +
tool_args(schema(json(), tool_schema))
[+ function.close if non-empty]
Wrapped in per-call markers (with optional parallel call repetition) then optionally in section markers.
build_tool_parser_tag_tagged(): For each tool function, builds one parser per argument:
- String types:
tool_arg_string_value(schema(until(value_suffix), ...)) - JSON types:
tool_arg_json_value(schema(json(), ...)) - Required args are plain; optional args wrapped in
optional() - Arguments joined with
space()between consecutive parsers
For closing: uses function.close if present; otherwise uses peek(per_call_end) to avoid premature close during partial streaming; falls back to tool_close(space()) to trigger mapper callbacks.
All three tool parsers return:
reasoning + optional(content(until(trigger_marker))) + tool_calls + end()
Each returned parser is wrapped by wrap_for_generation_prompt(), which prepends a literal for any boilerplate prefix of the generation prompt (the portion before the reasoning start marker).
Mapper
common_chat_peg_mapper maps PEG parse results (AST nodes) into common_chat_msg structures. Key design:
- Buffered arguments: Before
tool_nameis known, argument text goes toargs_buffer; once the name is set, the buffer is flushed tocurrent_tool->arguments args_target(): Returns a reference to whichever destination is currently active (buffer or tool args), eliminating branchingclosing_quote_pending: Tracks whether a closing"needs to be appended when a string argument value is finalized (for schema-declared string types in tagged format)- Whitespace-only reasoning: Reasoning content that consists entirely of whitespace (e.g. from a
<think></think>prefill) is cleared so the message shows no reasoning - Brace auto-closing: At tool close, unclosed
{braces are closed automatically
Files
| File | Purpose |
|---|---|
common/chat-auto-parser.h | All analysis structs, enums, autoparser, peg_generator, generation_params |
common/chat-auto-parser-generator.cpp | Parser generator: generate_parser() and build_parser() methods |
common/chat-diff-analyzer.cpp | Differential analysis implementation and workarounds |
common/chat-auto-parser-helpers.h/cpp | calculate_diff_split(), segmentize_markers(), compare_variants(), |
wrap_for_generation_prompt(), string helpers | |
common/chat-peg-parser.h/cpp | common_chat_peg_builder, common_chat_peg_mapper, and helpers |
common/chat.cpp | Entry point: common_chat_templates_apply_jinja() |
tests/test-chat-auto-parser.cpp | Auto-parser unit tests; also a debug tool when given a template path |
tests/test-chat-analysis.cpp | Template differential analysis debug tool |
Testing & Debugging
Debug Tools
Template Debugger: tests/test-chat-auto-parser.cpp
- Usage:
./bin/test-chat-auto-parser path/to/template.jinja(without a path, it runs the automated tests) - Shows detected format, markers, generated parser, and GBNF grammar
Template Analysis: tests/test-chat-analysis.cpp
- Usage:
./bin/test-chat-analysis --template-file path/to/template.jinja(without arguments, it runs on all templates from the test suite)
Debug Logging: Enable with LLAMA_ARG_LOG_VERBOSITY=2
- Shows detailed analysis steps, pattern extraction results, and generated parser structure
PEG Test Builder: Fluent API for creating test cases — see tests/test-chat.cpp:947-1043. Example usage:
auto tst = peg_tester ("models/templates/Template.jinja" );tst .test ("input text" ) .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) .tools ({tool_json }) .parallel_tool_calls (true) .enable_thinking (true) .expect (expected_message ) .run ();
Tested Templates
The following templates have active tests in tests/test-chat.cpp:
| Template | Format | Notes |
|---|---|---|
| Ministral-3-14B-Reasoning | Reasoning | [THINK]...[/THINK] tags (specialized handler) |
| NVIDIA-Nemotron-3-Nano-30B | TAG_WITH_TAGGED | Reasoning + tools |
| CohereForAI Command-R7B | JSON_NATIVE | <|START_THINKING|>/<|START_RESPONSE|> markers |
| Google Gemma 2 2B | Content only | No tool support |
| Qwen-QwQ-32B | Reasoning | Forced-open thinking |
| NousResearch Hermes 2 Pro | JSON_NATIVE | <tool_call> wrapper |
| IBM Granite 3.3 | JSON_NATIVE | <think></think> + <response></response> |
| IBM Granite 4.0 | JSON_NATIVE | <tool_call> wrapper (same template used by 4.1) |
| ByteDance Seed-OSS | TAG_WITH_TAGGED | Custom <seed:think> and <seed:tool_call> tags |
| Qwen3-Coder | TAG_WITH_TAGGED | XML-style tool format |
| DeepSeek V3.1 | JSON_NATIVE | Forced thinking mode |
| GLM-4.6 | TAG_WITH_TAGGED | <tool_call>name\n<arg_key>...<arg_value>... format |
| GLM-4.7-Flash | TAG_WITH_TAGGED | Updated GLM format |
| Kimi-K2-Thinking | JSON_NATIVE | Reasoning + JSON tools |
| Apertus-8B-Instruct | JSON_NATIVE | Function name as JSON key |
| MiniMax-M2 | TAG_WITH_JSON | XML invoke with JSON args |
| NVIDIA-Nemotron-Nano-v2 | JSON_NATIVE | <TOOLCALL> wrapper (nested) |
| CohereForAI Command-R Plus | JSON_NATIVE | Markdown code block format |
| Mistral-Nemo-Instruct-2407 | JSON_NATIVE | [TOOL_CALLS] wrapper with ID field |
| Functionary v3.1 | TAG_WITH_JSON | <function=X> format |
| Functionary v3.2 | Specialized | >>> recipient delimiter (dedicated handler) |
| Fireworks Firefunction v2 | TAG_WITH_JSON | Fireworks tool format |
| DeepSeek R1 Distill (Llama/Qwen) | Reasoning | Forced-open thinking |
| llama-cpp-deepseek-r1 | Reasoning | Forced-open thinking |
| Kimi-K2 / Kimi-K2-Instruct | JSON_NATIVE | JSON tools with special markers |
| Llama 3.1/3.2/3.3 | JSON_NATIVE | Standard Llama tool format |
| OpenAI GPT-OSS | Specialized | Channel-based (dedicated handler) |
| Apriel 1.5 | JSON_NATIVE | <tool_calls> wrapper with JSON array |
| Apriel 1.6 Thinker | Reasoning | Implicit reasoning start |
| Mistral Small 3.2 | JSON_NATIVE | [TOOL_CALLS]func[ARGS]{...} with call ID |
| Devstral | JSON_NATIVE | [TOOL_CALLS]func[ARGS]{...} without call ID |
| StepFun 3.5 Flash | TAG_WITH_TAGGED | <function=X><parameter=Y> format |
| Spark2.5 | TAG_WITH_TAGGED | <tool_call>name<arg_key>...<arg_value>... format |
Adding Support for New Templates
To support a new template format:
- If it follows standard patterns — The auto-parser should detect it automatically. Run
test-chat-auto-parser <template_path>to verify markers are correctly extracted. - If differential analysis extracts incorrect markers — Add a workaround lambda to the
workaroundsvector incommon/chat-diff-analyzer.cpp. Inspect the template source for a unique identifying substring. - If it needs fundamentally different handling — Add a dedicated handler function in
chat.cppbefore the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral).
Edge Cases and Quirks
- Generation Prompt & Reasoning Prefill: The generation prompt is extracted by diffing
add_generation_prompt=falsevstrueincommon_chat_templates_apply_jinja, so it contains exactly what the template appends — avoiding false positives from prior conversation turns. - Per-Call vs Per-Section Markers: Some templates wrap each tool call individually (
per_call_start/end); others wrap the entire section (section_start/end). T2 (check_per_call_markers()) disambiguates by checking if the second call in a two-call output starts with the section marker. - Tag Boundary Fixing:
calculate_diff_split()iteratively adjusts prefix/suffix boundaries to avoid splitting<tag>or[marker]tokens, ensuring clean extraction. - Call ID Side Effects: When a call ID is detected,
per_call_endmay have been incorrectly set to include the call ID suffix. T7 clearsper_call_endin this case. - Tool Analysis Gating:
analyze_toolsis only constructed (and all tool analysis phases run) whenjinja_caps.supports_tool_callsis true. Within tool analysis,check_per_call_markers()(T2) only runs ifjinja_caps.supports_parallel_tool_calls. analyze_arguments()Gating: Within tool analysis, A1 and A2 (argument name/value marker extraction) only run forTAG_WITH_TAGGEDformat.extract_argument_separator()andextract_args_markers()run for all non-JSON_NATIVEformats.- Undetected Tool Format: If
analyze_toolsconcludes tool calling is supported but cannot determine the format,build_parser()logs an error and returnseps()(graceful degradation) rather than aborting.
1# Auto-Parser Architecture 2 3The auto-parser automatically analyzes chat templates to determine how to parse model outputs, including content, reasoning, and tool calls. 4 5## Overview 6 7The unified auto-parser uses a pure differential, compositional approach (inspired by the `git diff` algorithm) to analyze chat templates: 8 9**Core Philosophy**: 10 11- **Minimize Hardcoded Patterns**: All markers extracted through template comparison (the only heuristic is JSON detection to distinguish `JSON_NATIVE` from tag-based formats) 12- **Compositional Architecture**: Separate analyzer structs for reasoning, content, and tools — each responsible for its own analysis and parser construction 13 14**Analysis + Parser Building in Two Steps**: 15 161. `autoparser::autoparser tmpl_analysis(tmpl)` — runs all differential comparisons and populates the analysis structs 172. `autoparser::peg_generator::generate_parser(tmpl, generation_params, tmpl_analysis)` — uses the analysis to build a PEG parser and optional GBNF grammar 18 19## Data Structures 20 21All structs are defined in [common/chat-auto-parser.h](common/chat-auto-parser.h). 22 23### Top-Level: `autoparser` (main analyzer and generator) 24 25[common/chat-auto-parser.h:367-388](common/chat-auto-parser.h#L367-L388) — top-level analysis result aggregating `jinja_caps`, `reasoning`, `content`, and `tools` sub-analyses, plus `preserved_tokens` (union of all non-empty markers). 26 27### `analyze_reasoning` 28 29[common/chat-auto-parser.h:254-274](common/chat-auto-parser.h#L254-L274) — reasoning analysis result: `mode` enum, `start` marker (e.g. `<think>`), and `end` marker (e.g. `</think>`). 30 31### `analyze_content` 32 33[common/chat-auto-parser.h:280-295](common/chat-auto-parser.h#L280-L295) — content analysis result: `mode` enum, `start`/`end` markers, and `requires_nonnull_content` flag. 34 35### `analyze_tools` and its sub-structs 36 37- [common/chat-auto-parser.h:176-194](common/chat-auto-parser.h#L176-L194) — `tool_format_analysis`: `mode` enum, `section_start/end`, `per_call_start/end`, JSON field names (`function_field`, `name_field`, `args_field`, `id_field`, `gen_id_field`), and format flags (`fun_name_is_key`, `tools_array_wrapped`) 38- [common/chat-auto-parser.h:196-200](common/chat-auto-parser.h#L196-L200) — `tool_function_analysis`: `name_prefix`, `name_suffix`, `close` markers around function names 39- [common/chat-auto-parser.h:202-210](common/chat-auto-parser.h#L202-L210) — `tool_arguments_analysis`: `start/end` container markers, `name_prefix/suffix`, `value_prefix/suffix`, `separator` 40- [common/chat-auto-parser.h:212-217](common/chat-auto-parser.h#L212-L217) — `tool_id_analysis`: `pos` enum, `prefix`/`suffix` markers around call ID values 41- [common/chat-auto-parser.h:301-361](common/chat-auto-parser.h#L301-L361) — `analyze_tools`: aggregates the four sub-structs above 42 43### Enums 44 45**`reasoning_mode`**: How the template handles reasoning/thinking blocks. 46 47| Value | Description | 48|-----------------|-----------------------------------------------------------------------------------| 49| `NONE` | No reasoning markers detected | 50| `TAG_BASED` | Tag-based: `<think>...</think>` (start can be empty for delimiter-style formats) | 51| `TOOLS_ONLY` | Reasoning only appears in tool call responses, not plain content | 52 53**Generation Prompt & Reasoning Prefill**: Computed in `common_chat_templates_apply_jinja` before invoking either the specialized handlers or the auto-parser, by rendering the template twice — once with `add_generation_prompt=false` and once with `add_generation_prompt=true` — and storing the diff suffix as `generation_params::generation_prompt`. This string is propagated into `common_chat_params::generation_prompt` and `common_chat_parser_params::generation_prompt`. 54 55The generation prompt is prepended to model output before PEG parsing via `wrap_for_generation_prompt()`. The portion *before* the reasoning start marker (if any) is prepended as a literal to ensure any boilerplate added by the template is consumed. The full string is also fed to the grammar sampler via `llama_sampler_accept` (stored in `common_params_sampling::grammar_prefill`), advancing the grammar past tokens already in the prompt. It is used to determine the reasoning budget sampler's initial state — COUNTING if the prefill tokens begin with the reasoning start sequence (but don't also contain the end sequence), IDLE otherwise. 56 57**`grammar_prefill`** (`common_params_sampling`): The generation prompt string tokenized and accepted by the grammar sampler at init time. Only applied when `grammar_external` is false (i.e., the grammar was not set explicitly by the user). 58 59Three outcomes for reasoning-prefill handling (in `generate_parser()`): 60 611. **Start+end in generation prompt** (e.g. `<think></think>\n`): the parser sees reasoning as opened and immediately closed; whitespace-only reasoning content is discarded. 622. **Only start in generation prompt** (e.g. `<think>\n`): the parser sees reasoning as already open. 633. **Start marker present but not at the end** (e.g. Apriel's `<|begin_assistant|>` followed by boilerplate): the marker is a template artifact; the start literal is cleared so reasoning uses delimiter-style (end-only). For templates that ignore `add_generation_prompt` (empty diff), the rendered `data.prompt` is used as fallback — but only for non-TOOLS_ONLY modes, since in TOOLS_ONLY the start tag is model-generated and may appear in prior conversation turns. 64 65**`content_mode`**: How the template wraps assistant content. 66 67| Value | Description | 68|--------------------------|----------------------------------------------------------------| 69| `PLAIN` | No content markers | 70| `ALWAYS_WRAPPED` | Content always wrapped: `<response>...</response>` | 71| `WRAPPED_WITH_REASONING` | Content wrapped only when reasoning is present | 72 73**`tool_format`**: Classification of tool call structure. 74 75| Value | Description | 76|------------------|------------------------------------------------------------------| 77| `NONE` | No tool support detected | 78| `JSON_NATIVE` | Pure JSON: `{"name": "X", "arguments": {...}}` | 79| `TAG_WITH_JSON` | Tag-based with JSON args: `<function=X>{...}</function>` | 80| `TAG_WITH_TAGGED`| Tag-based with tagged args: `<param=key>value</param>` | 81 82**`call_id_position`**: Where call IDs appear in tag-based formats. 83 84| Value | Description | 85|--------------------------|----------------------------------------------| 86| `NONE` | No call ID support detected | 87| `PRE_FUNC_NAME` | Before function name | 88| `BETWEEN_FUNC_AND_ARGS` | Between function name and arguments | 89| `POST_ARGS` | After arguments | 90 91## Tool Calling Formats 92 93### JSON_NATIVE 94 95**Structure**: The entire tool call (function name, arguments, values) is in JSON format. Optional enclosing tags around the section. 96 97**Detection**: Function name appears inside a JSON structure (quotes preceded by `{` or `:`). 98 99**Examples**: 100 101Standard OpenAI-style: 102 103``` json 104<tool_call> 105{ "name" : "get_weather" , "arguments" : { "location" : "Paris" , "unit" : "celsius" }} 106</tool_call> 107``` 108 109Mistral Nemo with array wrapper: 110 111``` json 112[TOOL_CALLS] 113[{ "name" : "calculate" , "arguments" : { "expr" : "2+2" }}] 114``` 115 116Function name as JSON key (Apertus style): 117 118``` json 119{ "get_weather" : { "location" : "Paris" }} 120``` 121 122--- 123 124### TAG_WITH_JSON 125 126**Structure**: Function name is outside JSON, in tag attributes or XML-style tags. Arguments are a JSON object. 127 128**Detection**: Function name not in JSON, but argument names appear in JSON context. 129 130**Examples**: 131 132Functionary v3.1: 133 134``` xml 135< function = get_weather > {"location": "Paris", "unit": "celsius"} </ function > 136``` 137 138MiniMax: 139 140``` xml 141< minimax:tool_call > 142< tool_name > calculate </ tool_name > 143< arguments > {"expr": "2+2"} </ arguments > 144</ minimax:tool_call > 145``` 146 147--- 148 149### TAG_WITH_TAGGED 150 151**Structure**: Both function name and argument names are in XML-style tags. String values are unquoted; non-string values are JSON-formatted. 152 153**Detection**: Neither function name nor argument names appear in a JSON context. 154 155**Examples**: 156 157Qwen/Hermes XML format: 158 159``` xml 160< function = get_weather > 161< param = location > Paris </ param > 162< param = unit > celsius </ param > 163</ function > 164``` 165 166Mixed types: 167 168``` xml 169< function = calculate > 170< param = expr > 2+2 </ param > 171< param = precision > 2 </ param > 172< param = options > {"round": true} </ param > 173</ function > 174``` 175 176String values (`Paris`, `celsius`, `2+2`) are unquoted; `options` (object type) is JSON-formatted. 177 178--- 179 180## Analysis Flow 181 182``` text 183autoparser::autoparser(tmpl) 184| 185|-- Phase 1: analyze_reasoning(tmpl, jinja_caps.supports_tool_calls) 186| |-- R1: compare_reasoning_presence() — with/without reasoning_content field 187| |-- R2: compare_thinking_enabled() — enable_thinking=false vs true 188| '-- R3: compare_reasoning_scope() — reasoning+content vs reasoning+tools 189| (only if supports_tool_calls) 190| 191|-- Phase 2: analyze_content(tmpl, reasoning) 192| '-- C1: compares content-only vs tools output and content-only vs reasoning output 193| 194|-- Phase 3: analyze_tools(tmpl, jinja_caps, reasoning) 195| (skipped entirely if !jinja_caps.supports_tool_calls) 196| | 197| |-- T1: analyze_tool_calls() — no tools vs with tools; classifies format 198| | |-- JSON path → analyze_tool_call_format_json_native() 199| | '-- tag path → analyze_tool_call_format_non_json() 200| | 201| (if format != NONE and format != JSON_NATIVE:) 202| | 203| |-- T2: check_per_call_markers() — 1 call vs 2 calls; moves section→per-call if needed 204| | (only if supports_parallel_tool_calls) 205| | 206| |-- T3: extract_function_markers() — func_alpha vs func_beta; extracts name prefix/suffix/close 207| | 208| |-- T4: analyze_arguments() — (TAG_WITH_TAGGED only) 209| | |-- A1: extract_argument_name_markers() — arg_name_A vs arg_name_B 210| | '-- A2: extract_argument_value_markers() — value "XXXX" vs "YYYY" 211| | 212| |-- T5: extract_argument_separator() — 1 arg vs 2 args; finds separator between args 213| | 214| |-- T6: extract_args_markers() — 0 args vs 1 arg; finds args container markers 215| | 216| '-- T7: extract_call_id_markers() — call_id "call00001" vs "call99999" 217| 218'-- collect_preserved_tokens() — union of all non-empty markers 219| 220'-- apply workarounds() — post-hoc patches for edge-case templates 221| 222v 223autoparser (analysis result) 224| 225v 226autoparser::peg_generator::generate_parser(tmpl, inputs, analysis) 227|-- analysis.build_parser(inputs) — builds PEG parser arena 228| |-- reasoning.build_parser(ctx) — reasoning parser (mode-dependent) 229| |-- content.build_parser(ctx) — content parser (mode-dependent) 230| '-- tools.build_parser(ctx) — tool parser (dispatches by tool_format) 231| |-- build_tool_parser_json_native() 232| |-- build_tool_parser_tag_json() 233| '-- build_tool_parser_tag_tagged() 234| 235|-- Build GBNF grammar (if tools present and trigger_marker non-empty) 236'-- Set grammar_triggers from section_start or per_call_start 237| 238v 239common_chat_params (prompt, parser, grammar, triggers, preserved_tokens) 240``` 241 242## Entry Point 243 244The auto-parser is invoked in [common/chat.cpp:1280-1310](common/chat.cpp#L1280-L1310) in `common_chat_templates_apply_jinja`. A few specialized templates are handled first (Ministral/Magistral Large 3, GPT-OSS with `<|channel|>`, Functionary v3.2 with `>>>all`), then the auto-parser handles everything else via `autoparser::autoparser` + `peg_generator::generate_parser`. 245 246## Algorithm Details 247 248### Core Mechanism: Differential Comparison 249 250All analysis phases use the same factorized comparison function declared in [common/chat-auto-parser-helpers.h:68](common/chat-auto-parser-helpers.h#L68): 251 252``` cpp 253compare_variants( tmpl , params_A , params_modifier ) 254``` 255 256This creates variant B by applying a modifier lambda to a copy of `params_A`, renders both through the template, and computes a `diff_split` ([common/chat-auto-parser.h:28-37](common/chat-auto-parser.h#L28-L37)): 257 258- `prefix` — common prefix between A and B 259- `suffix` — common suffix between A and B 260- `left` — unique to variant A 261- `right` — unique to variant B 262 263The diff is computed via `calculate_diff_split()`, which finds the longest-common-prefix and longest-common-suffix, then iteratively moves incomplete `<...>` or `[...]` markers from the prefix/suffix into left/right until stable (tag boundary fixing). 264 265Text is segmentized into markers and non-marker fragments using `segmentize_markers()`, which splits on `<...>` and `[...]` boundaries. 266 267### Phase 1: Reasoning Analysis 268 269**R1 — `compare_reasoning_presence()`**: Compares assistant message with vs without a `reasoning_content` field. 270 271- Searches `diff.right` (output with reasoning) for the reasoning content needle 272- Uses PEG parsers to find surrounding markers: 273- If both pre/post markers found in `diff.right` → `TAG_BASED` 274- If both found but post marker only in the full output B → `TAG_BASED` (template forces markers; handled via prefill) 275- If only post marker found → `TAG_BASED` (delimiter-style, empty start) 276- Sets `reasoning.start` and `reasoning.end` 277 278**R2 — `compare_thinking_enabled()`**: Compares `enable_thinking=false` vs `true` with a generation prompt. 279 280- Detects template-added reasoning markers: `enable_thinking=true` appends a non-empty marker → sets `reasoning.start`, mode = `TAG_BASED` 281- Handles the reverse case (`enable_thinking=false` appends the marker instead): extracts both start (from the preceding segment) and end markers; mode = `TAG_BASED` 282- The reasoning prefill (markers added by the template) is later extracted in `common_chat_templates_apply_jinja` and prepended to model output before parsing 283 284**R3 — `compare_reasoning_scope()`**: Compares assistant message with reasoning+text-content vs reasoning+tool-calls. 285 286- Only runs if `jinja_caps.supports_tool_calls` 287- Detects `TOOLS_ONLY`: reasoning content present in B (with tools) but not in A (with text content) 288- Extracts reasoning markers from the tool call output using PEG parsers 289 290### Phase 2: Content Analysis 291 292**C1**: Two comparisons in the `analyze_content` constructor: 293 294- Comparison 1: content-only output vs tool-call output → `diff_tools` 295- Comparison 2: content-only output vs reasoning+empty-content output → `diff_reasoning` 296 297Classification logic: 298 299- `PLAIN`: `diff_tools.left` equals the response string (content is the entire diff, no wrapper) 300- `ALWAYS_WRAPPED`: markers found surrounding the content text in `pure_content` → extracts `start`/`end` 301 302### Phase 3: Tool Call Analysis 303 304**T1 — `analyze_tool_calls()`**: Compares no-tools vs with-tools output. 305 306- Extracts the tool call section as `diff.right` 307- Calls `analyze_tool_call_format()` which first strips reasoning markers from the haystack, then: 308- Calls `in_json_haystack()` for both function name and argument name needles 309- `in_json_haystack()` uses a PEG parser to check whether the needle appears in a JSON context (preceded by `{` or `:` with surrounding quotes) 310- If function name is in JSON → `JSON_NATIVE` → `analyze_tool_call_format_json_native()` 311- If function name not in JSON, arg name is in JSON → `TAG_WITH_JSON` 312- If neither in JSON → `TAG_WITH_TAGGED` 313- `analyze_tool_call_format_json_native()`: parses the JSON object, matches field values to needles to populate `name_field`, `args_field`, `id_field`, `gen_id_field`; detects `tools_array_wrapped`; extracts `section_start`/`section_end` 314- `analyze_tool_call_format_non_json()`: uses PEG parsers on the haystack to find up to two opening markers (section + per-call) then up to two closing markers 315 316**T2 — `check_per_call_markers()`**: Compares 1 call vs 2 calls. 317 318- Computes a secondary diff of the second call portion vs the common suffix 319- If the second call content starts with `section_start` → the section marker is actually per-call → moves `section_start/end` to `per_call_start/end` and clears the section markers 320 321**T3 — `extract_function_markers()`**: Compares function name `FUN_FIRST` vs `FUN_SECOND` (two different named functions). 322 323- Finds where the function name appears in `diff.left` 324- Extracts `function.name_prefix` from the common prefix up to the function marker, and `function.name_suffix` from after the name up to the next marker 325- Extends `name_suffix` into `diff.suffix` (to the first marker for TAG_WITH_TAGGED; to the first `{` or `[` for TAG_WITH_JSON) 326- Extracts `function.close` from after the last argument value up to the per-call/section end marker 327 328**T4 — `analyze_arguments()`** (TAG_WITH_TAGGED only): 329 330- **A1 `extract_argument_name_markers()`**: Compares `arg_name_A` vs `arg_name_B` (two different argument names). 331- Finds shared surrounding structure → `arguments.name_prefix`, `arguments.name_suffix` 332- **A2 `extract_argument_value_markers()`**: Compares argument value `"XXXX"` vs `"YYYY"` (same arg, different value). 333- Finds markers surrounding the value → `arguments.value_prefix`, `arguments.value_suffix` 334 335**T5 — `extract_argument_separator()`**: Compares 1 argument vs 2 arguments (same function). 336 337- Uses `until_common_prefix(diff.right, ARG_FIRST, ARG_SECOND)` to find what separates the two argument blocks 338 339**T6 — `extract_args_markers()`**: Compares 0 arguments vs 1 argument. 340 341- Uses `until_common_prefix()` and `after_common_suffix()` with the empty and single-arg JSON strings as anchors to find container markers (`arguments.start`, `arguments.end`) 342 343**T7 — `extract_call_id_markers()`**: Compares call IDs `"call00001"` vs `"call99999"`. 344 345- Determines whether function name appears in `diff.prefix` or `diff.suffix` to classify position: 346- Function name in prefix only → `BETWEEN_FUNC_AND_ARGS` or `POST_ARGS` (further distinguished by where `{` appears) 347- Function name in suffix only → `PRE_FUNC_NAME` 348- Extracts `call_id.prefix` and `call_id.suffix` markers around the call ID value 349- Clears `per_call_end` if it incorrectly incorporated the call ID suffix 350 351### Workarounds 352 353A workaround array in `common/chat-diff-analyzer.cpp` applies post-hoc patches after analysis. Each workaround is a lambda that inspects the template source and overrides analysis results. Current workarounds: 354 3551. **Old Qwen/DeepSeek thinking templates** — source contains `content.split('</think>')` but not `<SPECIAL_12>`: sets `reasoning.mode = TAG_BASED` with `<think>`/`</think>` markers if no reasoning was detected 3562. **Granite 3.3** — source contains specific "Write your thoughts" text: forces `TAG_BASED` reasoning with `<think>`/`</think>` and `WRAPPED_WITH_REASONING` content with `<response>`/`</response>` 3573. **Cohere Command R+** — source contains `<|CHATBOT_TOKEN|>`: sets `ALWAYS_WRAPPED` content mode if no content start is already set 3584. **Functionary 3.1** — source contains `set has_code_interpreter`: forces `PLAIN` content, specific `per_call_start/end`, clears preserved tokens to only keep Functionary-specific markers 3595. **DeepSeek-R1-Distill-Qwen** — source contains `tool▁calls▁begin` markers: overrides tool section/per-call markers with the correct Unicode block characters 360 361### Parser Building 362 363Each analyzer struct (`analyze_reasoning`, `analyze_content`, `analyze_tools`) implements `build_parser(parser_build_context&)`. They share a `parser_build_context` that carries the PEG builder, inference inputs, the pre-built reasoning parser, and a pointer to the content analyzer. 364 365#### Reasoning Parser (`analyze_reasoning::build_parser`) 366 367| Mode | Parser | 368|-----------------------------------------------|---------------------------------------------------------------------------| 369| Not extracting reasoning | `eps()` | 370| `TAG_BASED` or `TOOLS_ONLY` (non-empty start) | `optional(start + reasoning(until(end)) + end + space())` | 371| `TAG_BASED` or `TOOLS_ONLY` (empty start) | `optional(reasoning(until(end)) + end + space())` — delimiter-style | 372 373Note: The start marker may be empty either because the analyzer detected delimiter-style reasoning, or because `generate_parser()` cleared a template artifact start marker (see Generation Prompt & Reasoning Prefill above). Whitespace-only reasoning content (e.g. from a `<think></think>` prefill) is discarded by the mapper. 374 375#### Content Parser (`analyze_content::build_parser`) 376 377| Condition | Parser | 378|----------------------------------------|---------------------------------------------------------------------------------| 379| `json_schema` present | `reasoning + space() + content(schema(json(), "response-format", ...)) + end()` | 380| Tools present | Dispatches to `analyze_tools::build_parser()` | 381| `ALWAYS_WRAPPED` with reasoning | `reasoning + start + content(until(end)) + end + end()` | 382| `ALWAYS_WRAPPED` without reasoning | `content(until(start)) + start + content(until(end)) + end + end()` | 383| Default (PLAIN) | `reasoning + content(rest()) + end()` | 384 385#### Tool Parsers (`analyze_tools::build_parser`) 386 387Dispatches by `format.mode`: 388 389**`build_tool_parser_json_native()`**: Calls `p.standard_json_tools()` which internally dispatches to: 390 391- `build_json_tools_function_is_key()` — function name is the JSON key: `{"get_weather": {...}}` 392- `build_json_tools_nested_keys()` — nested: `{"function": {"name": "X", "arguments": {...}}}` 393- `build_json_tools_flat_keys()` — flat: `{"name": "X", "arguments": {...}}` 394 395Handles content wrappers, array wrapping (`tools_array_wrapped`), parallel calls, and `parameter_order`. 396 397**`build_tool_parser_tag_json()`**: For each tool function: 398 399``` text 400tool_open(name_prefix + tool_name(literal(name)) + name_suffix) + 401call_id_section + 402tool_args(schema(json(), tool_schema)) 403[+ function.close if non-empty] 404``` 405 406Wrapped in per-call markers (with optional parallel call repetition) then optionally in section markers. 407 408**`build_tool_parser_tag_tagged()`**: For each tool function, builds one parser per argument: 409 410- String types: `tool_arg_string_value(schema(until(value_suffix), ...))` 411- JSON types: `tool_arg_json_value(schema(json(), ...))` 412- Required args are plain; optional args wrapped in `optional()` 413- Arguments joined with `space()` between consecutive parsers 414 415For closing: uses `function.close` if present; otherwise uses `peek(per_call_end)` to avoid premature close during partial streaming; falls back to `tool_close(space())` to trigger mapper callbacks. 416 417All three tool parsers return: 418 419``` text 420reasoning + optional(content(until(trigger_marker))) + tool_calls + end() 421``` 422 423Each returned parser is wrapped by `wrap_for_generation_prompt()`, which prepends a literal for any boilerplate prefix of the generation prompt (the portion before the reasoning start marker). 424 425## Mapper 426 427`common_chat_peg_mapper` maps PEG parse results (AST nodes) into `common_chat_msg` structures. Key design: 428 429- **Buffered arguments**: Before `tool_name` is known, argument text goes to `args_buffer`; once the name is set, the buffer is flushed to `current_tool->arguments` 430- **`args_target()`**: Returns a reference to whichever destination is currently active (buffer or tool args), eliminating branching 431- **`closing_quote_pending`**: Tracks whether a closing `"` needs to be appended when a string argument value is finalized (for schema-declared string types in tagged format) 432- **Whitespace-only reasoning**: Reasoning content that consists entirely of whitespace (e.g. from a `<think></think>` prefill) is cleared so the message shows no reasoning 433- **Brace auto-closing**: At tool close, unclosed `{` braces are closed automatically 434 435## Files 436 437| File | Purpose | 438|-------------------------------------------|---------------------------------------------------------------------------------| 439| `common/chat-auto-parser.h` | All analysis structs, enums, `autoparser`, `peg_generator`, `generation_params` | 440| `common/chat-auto-parser-generator.cpp` | Parser generator: `generate_parser()` and `build_parser()` methods | 441| `common/chat-diff-analyzer.cpp` | Differential analysis implementation and workarounds | 442| `common/chat-auto-parser-helpers.h/cpp` | `calculate_diff_split()`, `segmentize_markers()`, `compare_variants()`, | 443| | `wrap_for_generation_prompt()`, string helpers | 444| `common/chat-peg-parser.h/cpp` | `common_chat_peg_builder`, `common_chat_peg_mapper`, and helpers | 445| `common/chat.cpp` | Entry point: `common_chat_templates_apply_jinja()` | 446| `tests/test-chat-auto-parser.cpp` | Auto-parser unit tests; also a debug tool when given a template path | 447| `tests/test-chat-analysis.cpp` | Template differential analysis debug tool | 448 449## Testing & Debugging 450 451### Debug Tools 452 453**Template Debugger**: `tests/test-chat-auto-parser.cpp` 454 455- Usage: `./bin/test-chat-auto-parser path/to/template.jinja` (without a path, it runs the automated tests) 456- Shows detected format, markers, generated parser, and GBNF grammar 457 458**Template Analysis**: `tests/test-chat-analysis.cpp` 459 460- Usage: `./bin/test-chat-analysis --template-file path/to/template.jinja` (without arguments, it runs on all templates from the test suite) 461 462**Debug Logging**: Enable with `LLAMA_ARG_LOG_VERBOSITY=2` 463 464- Shows detailed analysis steps, pattern extraction results, and generated parser structure 465 466**PEG Test Builder**: Fluent API for creating test cases — see [tests/test-chat.cpp:947-1043](tests/test-chat.cpp#L947-L1043). Example usage: 467 468``` cpp 469auto tst = peg_tester ( "models/templates/Template.jinja" ); 470tst. test ( "input text" ) 471. reasoning_format ( COMMON_REASONING_FORMAT_AUTO ) 472. tools ({ tool_json }) 473. parallel_tool_calls (true) 474. enable_thinking (true) 475. expect ( expected_message ) 476. run (); 477``` 478 479### Tested Templates 480 481The following templates have active tests in `tests/test-chat.cpp`: 482 483| Template | Format | Notes | 484| -------- | ------ | ----- | 485| Ministral-3-14B-Reasoning | Reasoning | `[THINK]...[/THINK]` tags (specialized handler) | 486| NVIDIA-Nemotron-3-Nano-30B | TAG_WITH_TAGGED | Reasoning + tools | 487| CohereForAI Command-R7B | JSON_NATIVE | `<\|START_THINKING\|>`/`<\|START_RESPONSE\|>` markers | 488| Google Gemma 2 2B | Content only | No tool support | 489| Qwen-QwQ-32B | Reasoning | Forced-open thinking | 490| NousResearch Hermes 2 Pro | JSON_NATIVE | `<tool_call>` wrapper | 491| IBM Granite 3.3 | JSON_NATIVE | `<think></think>` + `<response></response>` | 492| IBM Granite 4.0 | JSON_NATIVE | `<tool_call>` wrapper (same template used by 4.1) | 493| ByteDance Seed-OSS | TAG_WITH_TAGGED | Custom `<seed:think>` and `<seed:tool_call>` tags | 494| Qwen3-Coder | TAG_WITH_TAGGED | XML-style tool format | 495| DeepSeek V3.1 | JSON_NATIVE | Forced thinking mode | 496| GLM-4.6 | TAG_WITH_TAGGED | `<tool_call>name\n<arg_key>...<arg_value>...` format | 497| GLM-4.7-Flash | TAG_WITH_TAGGED | Updated GLM format | 498| Kimi-K2-Thinking | JSON_NATIVE | Reasoning + JSON tools | 499| Apertus-8B-Instruct | JSON_NATIVE | Function name as JSON key | 500| MiniMax-M2 | TAG_WITH_JSON | XML invoke with JSON args | 501| NVIDIA-Nemotron-Nano-v2 | JSON_NATIVE | `<TOOLCALL>` wrapper (nested) | 502| CohereForAI Command-R Plus | JSON_NATIVE | Markdown code block format | 503| Mistral-Nemo-Instruct-2407 | JSON_NATIVE | `[TOOL_CALLS]` wrapper with ID field | 504| Functionary v3.1 | TAG_WITH_JSON | `<function=X>` format | 505| Functionary v3.2 | Specialized | `>>>` recipient delimiter (dedicated handler) | 506| Fireworks Firefunction v2 | TAG_WITH_JSON | Fireworks tool format | 507| DeepSeek R1 Distill (Llama/Qwen) | Reasoning | Forced-open thinking | 508| llama-cpp-deepseek-r1 | Reasoning | Forced-open thinking | 509| Kimi-K2 / Kimi-K2-Instruct | JSON_NATIVE | JSON tools with special markers | 510| Llama 3.1/3.2/3.3 | JSON_NATIVE | Standard Llama tool format | 511| OpenAI GPT-OSS | Specialized | Channel-based (dedicated handler) | 512| Apriel 1.5 | JSON_NATIVE | `<tool_calls>` wrapper with JSON array | 513| Apriel 1.6 Thinker | Reasoning | Implicit reasoning start | 514| Mistral Small 3.2 | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` with call ID | 515| Devstral | JSON_NATIVE | `[TOOL_CALLS]func[ARGS]{...}` without call ID | 516| StepFun 3.5 Flash | TAG_WITH_TAGGED | `<function=X><parameter=Y>` format | 517| Spark2.5 | TAG_WITH_TAGGED | `<tool_call>name<arg_key>...<arg_value>...` format | 518 519## Adding Support for New Templates 520 521To support a new template format: 522 5231. **If it follows standard patterns** — The auto-parser should detect it automatically. Run `test-chat-auto-parser <template_path>` to verify markers are correctly extracted. 5242. **If differential analysis extracts incorrect markers** — Add a workaround lambda to the `workarounds` vector in `common/chat-diff-analyzer.cpp`. Inspect the template source for a unique identifying substring. 5253. **If it needs fundamentally different handling** — Add a dedicated handler function in `chat.cpp` before the auto-parser block (as done for GPT-OSS, Functionary v3.2, and Ministral). 526 527## Edge Cases and Quirks 528 5291. **Generation Prompt & Reasoning Prefill**: The generation prompt is extracted by diffing `add_generation_prompt=false` vs `true` in `common_chat_templates_apply_jinja`, so it contains exactly what the template appends — avoiding false positives from prior conversation turns. 5302. **Per-Call vs Per-Section Markers**: Some templates wrap each tool call individually (`per_call_start/end`); others wrap the entire section (`section_start/end`). T2 (`check_per_call_markers()`) disambiguates by checking if the second call in a two-call output starts with the section marker. 5313. **Tag Boundary Fixing**: `calculate_diff_split()` iteratively adjusts prefix/suffix boundaries to avoid splitting `<tag>` or `[marker]` tokens, ensuring clean extraction. 5324. **Call ID Side Effects**: When a call ID is detected, `per_call_end` may have been incorrectly set to include the call ID suffix. T7 clears `per_call_end` in this case. 5335. **Tool Analysis Gating**: `analyze_tools` is only constructed (and all tool analysis phases run) when `jinja_caps.supports_tool_calls` is true. Within tool analysis, `check_per_call_markers()` (T2) only runs if `jinja_caps.supports_parallel_tool_calls`. 5346. **`analyze_arguments()` Gating**: Within tool analysis, A1 and A2 (argument name/value marker extraction) only run for `TAG_WITH_TAGGED` format. `extract_argument_separator()` and `extract_args_markers()` run for all non-`JSON_NATIVE` formats. 5357. **Undetected Tool Format**: If `analyze_tools` concludes tool calling is supported but cannot determine the format, `build_parser()` logs an error and returns `eps()` (graceful degradation) rather than aborting.