Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Ignore generation/endgeneration tags when analyzing Jinja chat template#2787

Merged
winglian merged 5 commits intoaxolotl-ai-cloud:mainfrom
nyxkrage:ignore-gen-tags
Jun 18, 2025
Merged

Ignore generation/endgeneration tags when analyzing Jinja chat template#2787
winglian merged 5 commits intoaxolotl-ai-cloud:mainfrom
nyxkrage:ignore-gen-tags

Conversation

@nyxkrage
Copy link
Contributor

@nyxkrage nyxkrage commented Jun 13, 2025

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

  • New Features
    • Added support for ignoring {% generation %} and {% endgeneration %} tags in Jinja2 templates, allowing templates with these tags to be parsed without errors.
    • Introduced a new structured chat template "phi_4" that enables stepwise reasoning with distinct "Thought" and "Solution" sections for clearer and more accurate responses.
  • Bug Fixes
    • Refined chat turn handling to better support specific tokenizer types by narrowing conditions for skipping system turns.
  • Tests
    • Extended test coverage to include new and existing tokenizer configurations, ensuring compatibility with the new chat template and updated turn-skipping logic.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 13, 2025

"""

Walkthrough

A new Jinja2 extension, GenerationTagIgnore, is introduced to ignore {% generation %} and {% endgeneration %} tags during template parsing. The JinjaTemplateAnalyzer class is updated to use this extension, allowing templates with these tags to be parsed without error and the tags to be skipped during analysis. Additionally, a new "phi_4" chat template is added with structured reasoning and response format, and related tokenizer and test updates are made. The find_turn method's condition is simplified to only check for "mistral" tokenizer.

Changes

File(s) Change Summary
src/axolotl/prompt_strategies/jinja_template_analyzer.py Added GenerationTagIgnore Jinja2 extension to skip generation tags; updated analyzer to use this extension.
src/axolotl/utils/chat_templates.py Added new "phi_4" chat template with structured prompt format including Thought and Solution sections.
src/axolotl/prompt_strategies/chat_template.py Simplified find_turn method to only check for "mistral" tokenizer instead of "mistral" or "gemma".
tests/prompt_strategies/conftest.py Added phi4_tokenizer pytest fixture loading "microsoft/Phi-4-reasoning" tokenizer.
tests/prompt_strategies/test_chat_templates_advanced.py Added phi4_tokenizer and re-enabled gemma2_tokenizer in test parameters; adjusted skipping logic; added assertions for "phi_4" template variables.

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)
Loading

Assessment against linked issues

Objective Addressed Explanation
Ignore {% generation %} and {% endgeneration %} tags in chat templates to prevent parsing errors (#2762)

Poem

In templates where tags would cause a fright,
Now "generation" vanishes out of sight.
The analyzer hops with glee,
Parsing smoothly, bug-free!
With every hop and every tag ignored,
The codebase is now perfectly restored.
🐇✨
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51ecc7d and 456c063.

📒 Files selected for processing (5)
  • src/axolotl/prompt_strategies/chat_template.py (1 hunks)
  • src/axolotl/prompt_strategies/jinja_template_analyzer.py (3 hunks)
  • src/axolotl/utils/chat_templates.py (1 hunks)
  • tests/prompt_strategies/conftest.py (1 hunks)
  • tests/prompt_strategies/test_chat_templates_advanced.py (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/axolotl/prompt_strategies/jinja_template_analyzer.py
  • src/axolotl/prompt_strategies/chat_template.py
  • src/axolotl/utils/chat_templates.py
  • tests/prompt_strategies/conftest.py
  • tests/prompt_strategies/test_chat_templates_advanced.py
⏰ Context from checks skipped due to timeout of 90000ms (8)
  • GitHub Check: PyTest from Source Dist (3.11, 2.7.1)
  • GitHub Check: PyTest (3.11, 2.7.1)
  • GitHub Check: PyTest from Source Dist (3.11, 2.6.0)
  • GitHub Check: PyTest (3.11, 2.5.1)
  • GitHub Check: pre-commit
  • GitHub Check: PyTest (3.11, 2.6.0)
  • GitHub Check: PyTest from Source Dist (3.11, 2.5.1)
  • GitHub Check: pre-commit
✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. Using next(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 hook

Hard-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 optional extra_extensions parameter 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)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2274d4 and 3e20b5c.

📒 Files selected for processing (1)
  • src/axolotl/prompt_strategies/jinja_template_analyzer.py (3 hunks)

Comment on lines +29 to +37
class GenerationTagIgnore(Extension):
tags = {"generation", "endgeneration"}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

@NanoCode012
Copy link
Collaborator

This does fix the generation tag error but I seem to receive jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' afterward (could be upstream template issue). Did you get pass it for phi4 template?

@codecov
Copy link

codecov bot commented Jun 13, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

📢 Thoughts on this report? Let us know!

@nyxkrage
Copy link
Contributor Author

nyxkrage commented Jun 13, 2025

This does fix the generation tag error but I seem to receive jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'content' afterward (could be upstream template issue). Did you get pass it for phi4 template?

Yeah, it passes fine for me, I even just recloned and created a new venv, and it still works.
What version of jinja2 are you on? I have 3.1.6 on my fresh install.
The chat template that axolotl logs out for the model for me is:

<|im_start|>system<|im_sep|>You are Phi, a language model trained by Microsoft to help users. Your role as an assistant involves thoroughly exploring questions through a systematic thinking process before providing the final precise and accurate solutions. This requires engaging in a comprehensive cycle of analysis, summarizing, exploration, reassessment, reflection, backtracing, and iteration to develop well-considered thinking process. Please structure your response into two main sections: Thought and Solution using the specified format: <think> {Thought section} </think> {Solution section}. In the Thought section, detail your reasoning process in steps. Each step should include detailed considerations such as analysing questions, summarizing relevant findings, brainstorming new ideas, verifying the accuracy of the current steps, refining any errors, and revisiting previous steps. In the Solution section, based on various attempts, explorations, and reflections from the Thought section, systematically present the final solution that you deem correct. The Solution section should be logical, accurate, and concise and detail necessary steps needed to reach the conclusion. Now, try to solve the following question through the above guidelines:<|im_end|>{% for message in messages %}{% if (message['role'] == 'user') %}{{'<|im_start|>user<|im_sep|>' + message['content'] + '<|im_end|>'}}{% elif (message['role'] == 'assistant') %}{{'<|im_start|>assistant<|im_sep|>'}}{% generation %}{{message['content'] + '<|im_end|>'}}{% endgeneration %}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant<|im_sep|>' }}{% endif %}

Also regarding the coderabbit comment, if you only set generation it does not also match endgeneration. And you end up with

[2025-06-13 19:21:27,770] [ERROR] [axolotl.prompt_strategies.load:49] [PID:1262331] [RANK:0] Failed to load prompt strategy `chat_template`: Encountered unknown tag 'endgeneration'. Jinja was looking for the following tags: 'elif' or 'else' or 'endif'. The innermost block that needs to be closed is 'if'.

@winglian
Copy link
Collaborator

Are there any test cases we can add?

@NanoCode012
Copy link
Collaborator

@nyxkrage , thanks. It was an error on my part. I was using an incompat function calling dataset.

I added phi4 test and customized their chat template a bit to accept custom system prompts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/axolotl/utils/chat_templates.py (1)

35-35: phi_4 template – edge-cases & maintainability review

The template works but a couple of minor issues surfaced:

  1. 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.

  2. Unhandled roles – the loop omits handling for a second system or a future tool role. That might be intentional, but it differs from other built-in templates. If unexpected roles can appear, add an else branch that raises to aid debugging.

  3. 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 duplication

We 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.param

and update the test with @pytest.mark.parametrize(*chat_template_case.__annotations__.keys()).

This keeps the test readable and makes additions like gemma2_tokenizer / phi4_tokenizer one-liners instead of 4-liners scattered in the middle of a giant list.


95-98: _should_skip_turn hard-codes tokenizer name – make it declarative

The 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: Duplicate elif branches – collapse into a single membership check

These 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3e20b5c and ba10c73.

📒 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_turn now skips the “system-only turn” optimisation exclusively for tokenizers whose name_or_path contains “mistral”.
Gemma/Gemma-3 tokenizers were previously covered by the same shortcut. If a Gemma conversation still begins with a single system message the method now tries to compute token boundaries and may fail (empty prompt, boundaries -1, etc.).

Before merging, please confirm that:

  1. All Gemma tokenizers generate a first user turn even when the first message is system, or
  2. 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' | head

If 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()
+            )

nyxkrage and others added 5 commits June 17, 2025 08:34
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.
@nyxkrage
Copy link
Contributor Author

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.

@winglian winglian merged commit eb3a57e into axolotl-ai-cloud:main Jun 18, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat Templates With Generation Tags Currently Fail

3 participants