Ignore generation/endgeneration tags when analyzing Jinja chat template#2787
Ignore generation/endgeneration tags when analyzing Jinja chat template#2787winglian merged 5 commits intoaxolotl-ai-cloud:mainfrom
Conversation
|
""" WalkthroughA new Jinja2 extension, Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant JinjaTemplateAnalyzer
participant Jinja2Environment
participant GenerationTagIgnore
User->>JinjaTemplateAnalyzer: Initialize with template
JinjaTemplateAnalyzer->>Jinja2Environment: Create environment with GenerationTagIgnore
JinjaTemplateAnalyzer->>Jinja2Environment: Parse template
Jinja2Environment->>GenerationTagIgnore: Encounter {% generation %} tag
GenerationTagIgnore-->>Jinja2Environment: Ignore tag, return empty node
Jinja2Environment-->>JinjaTemplateAnalyzer: Return parsed AST (tags ignored)
Assessment against linked issues
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (5)
⏰ Context from checks skipped due to timeout of 90000ms (8)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/axolotl/prompt_strategies/jinja_template_analyzer.py (2)
32-34: Consume the tag token explicitly for clarity
parser.stream.skip(1)silently advances the stream but obscures intent. Usingnext(parser.stream)(as in Jinja’s own examples) makes it obvious that the tag token is consumed and conveys the current lineno for potential error reporting.- def parse(self, parser): - parser.stream.skip(1) - return nodes.Const("") + def parse(self, parser): + # Discard the tag token and produce an empty node + next(parser.stream) # consume 'generation' + return nodes.Const("")
66-66: Offer an extension-injection hookHard-coding
extensions=[GenerationTagIgnore]makes the analyser less composable for downstream users who might already be passing custom extensions (e.g., i18n or sandbox). Accepting an optionalextra_extensionsparameter and merging it would avoid forcing callers to fork this class.- def __init__(self, template: str): - self.env: Environment = Environment(autoescape=True, extensions=[GenerationTagIgnore]) + def __init__(self, template: str, *, extra_extensions: Optional[list[type[Extension]]] = None): + exts = [GenerationTagIgnore] + if extra_extensions: + exts.extend(extra_extensions) + self.env: Environment = Environment(autoescape=True, extensions=exts)
| class GenerationTagIgnore(Extension): | ||
| tags = {"generation", "endgeneration"} | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Document & scope the tag list
endgeneration does not need to be declared as a standalone tag; Jinja automatically recognises an end<name> token as the terminator of the <name> block when the block-level tag is registered.
Keeping it in tags means the engine will treat {% endgeneration %} as another opening tag which is then ignored, rather than as a block terminator. Functionally it works because both tags are discarded, but it disables Jinja’s usual mismatch checks (e.g. {% generation %}{% endif %} will no longer raise). Consider restricting the set to {"generation"} to keep the parser’s safety rails.
-class GenerationTagIgnore(Extension):
- tags = {"generation", "endgeneration"}
+class GenerationTagIgnore(Extension):
+ """Ignore the `{% generation %}` block (Axolotl masks assistant turns itself)."""
+ # Only the opening tag needs registration; the matching end tag is implicit.
+ tags = {"generation"}🤖 Prompt for AI Agents
In src/axolotl/prompt_strategies/jinja_template_analyzer.py around lines 29 to
31, the tags set in the GenerationTagIgnore class includes "endgeneration"
unnecessarily. Remove "endgeneration" from the tags set, leaving only
{"generation"}, so Jinja can correctly recognize {% endgeneration %} as the
block terminator and maintain its built-in syntax checks.
|
This does fix the |
Codecov ReportAll modified and coverable lines are covered by tests ✅ 📢 Thoughts on this report? Let us know! |
Yeah, it passes fine for me, I even just recloned and created a new venv, and it still works. Also regarding the coderabbit comment, if you only set generation it does not also match endgeneration. And you end up with |
|
Are there any test cases we can add? |
|
@nyxkrage , thanks. It was an error on my part. I was using an incompat function calling dataset. I added |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
src/axolotl/utils/chat_templates.py (1)
35-35:phi_4template – edge-cases & maintainability reviewThe template works but a couple of minor issues surfaced:
New-line consistency – the
<|im_start|>assistant<|im_sep|>token is concatenated directly with the following{% generation %}block. Lack of a newline or space may yield<|im_sep|><think>if the assistant’s content starts with<think>. Consider adding an explicit'\n'after the separator to ensure clean token boundaries.Unhandled roles – the loop omits handling for a second
systemor a futuretoolrole. That might be intentional, but it differs from other built-in templates. If unexpected roles can appear, add anelsebranch that raises to aid debugging.Readability – the gigantic default system prompt makes the template hard to scan. Extracting it into a dedicated variable keeps the Jinja logic short:
-{% else %}You are Phi, a language model ... +{% else %} +{% set default_phi_prompt %} +You are Phi, a language model ... +(unchanged text) +{% endset %} +{{ default_phi_prompt }} {% endif %}(Not blocking, but will help future maintenance.)
tests/prompt_strategies/test_chat_templates_advanced.py (3)
36-44: Parameter list keeps ballooning – extract to data fixture to avoid duplicationWe now hard-code 9-tuple entries; each new tokenizer / template combination repeats 4 strings.
Maintaining this inline list is error-prone (ordering, duplicates, forgotten cleanup).
Consider moving the matrix to a small JSON / YAML or a pytest fixture that yields the tuples, e.g.# conftest.py CHAT_TEMPLATE_PARAMS = [ ("llama3_tokenizer", "llama3", None, None), ... ] @pytest.fixture(params=CHAT_TEMPLATE_PARAMS, ids=lambda p: p[0]) def chat_template_case(request): return request.paramand update the test with
@pytest.mark.parametrize(*chat_template_case.__annotations__.keys()).This keeps the test readable and makes additions like
gemma2_tokenizer/phi4_tokenizerone-liners instead of 4-liners scattered in the middle of a giant list.
95-98:_should_skip_turnhard-codes tokenizer name – make it declarativeThe skip condition is now limited to
"mistral"tokenizers, but the string test is embedded in the method:and ("mistral" in tokenizer.name_or_path.lower())Encapsulate the knowledge of “which tokenizers elide the first system/context turn” in a small set for maintainability:
+_SYSTEM_SKIP_TOKENIZERS = {"mistral"} ... - and ("mistral" in tokenizer.name_or_path.lower()) + and any(t in tokenizer.name_or_path.lower() for t in _SYSTEM_SKIP_TOKENIZERS)Adding future models becomes a one-line update to the set instead of a code change.
963-968: Duplicateelifbranches – collapse into a single membership checkThese two branches perform the identical assertion; Ruff rightly flags
SIM114.
You can combine them for brevity and to avoid future copy-paste errors:- elif chat_template == "phi_35": - assert variables == {"role", "content"}, ( - f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" - f"Got: {variables}\n" - f"Chat template: {actual_jinja_template}" - ) - elif chat_template == "phi_4": + elif chat_template in {"phi_35", "phi_4"}: assert variables == {"role", "content"}, ( f"Expected variables: {'role', 'content'} from {tokenizer}/{chat_template}\n" f"Got: {variables}\n" f"Chat template: {actual_jinja_template}" )This removes redundant code and satisfies the static-analysis suggestion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/axolotl/prompt_strategies/chat_template.py(1 hunks)src/axolotl/utils/chat_templates.py(1 hunks)tests/prompt_strategies/conftest.py(1 hunks)tests/prompt_strategies/test_chat_templates_advanced.py(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- tests/prompt_strategies/conftest.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/axolotl/prompt_strategies/chat_template.py (1)
src/axolotl/utils/mistral_tokenizer.py (1)
name_or_path(135-136)
🪛 Ruff (0.11.9)
tests/prompt_strategies/test_chat_templates_advanced.py
957-968: Combine if branches using logical or operator
Combine if branches
(SIM114)
🔇 Additional comments (1)
src/axolotl/prompt_strategies/chat_template.py (1)
595-601: Verify removal of the “gemma” guard – possible regression for Gemma tokenizers
find_turnnow skips the “system-only turn” optimisation exclusively for tokenizers whosename_or_pathcontains “mistral”.
Gemma/Gemma-3 tokenizers were previously covered by the same shortcut. If a Gemma conversation still begins with a singlesystemmessage the method now tries to compute token boundaries and may fail (empty prompt, boundaries-1, etc.).Before merging, please confirm that:
- All Gemma tokenizers generate a first user turn even when the first message is
system, or- Tests exercising Gemma paths still pass after this change.
A quick repo-wide check helps:
#!/bin/bash # Look for tokenizer initialisation mentioning "gemma" to spot custom subclasses rg -A2 -i 'gemma' | headIf Gemma still needs the shortcut, re-introduce the check:
- and ("mistral" in self.tokenizer.name_or_path.lower()) + and ( + "mistral" in self.tokenizer.name_or_path.lower() + or "gemma" in self.tokenizer.name_or_path.lower() + )
Axolotl handles calculating the mask for assistant turns on its own, and as such these tags are not needed, however currently the analyzer does not recognize them at all and throws an error.
51ecc7d to
456c063
Compare
|
Thanks for taking the time to add some tests! I got a bit busy on a sidequest, so didn't end up getting back to this. |
This adds a small extension to the jinja anyalyzer environment that skips over any
{% generation %}or{% endgeneration %}tags.Motivation and Context
Axolotl handles calculating the mask for assistant turns on its own, and as such these tags are not needed, however currently the analyzer does not recognize them at all and throws an error.
This fixes #2762
How has this been tested?
I tested this by running the problematic config in #2762, and it no longer errors and properly trains.
Summary by CodeRabbit
{% generation %}and{% endgeneration %}tags in Jinja2 templates, allowing templates with these tags to be parsed without errors.