pub package pub points Build Status License: MIT codecov

dinja

dinja renders LLM chat templates in Dart: give it a model's Jinja chat template and a conversation, and it returns the prompt string. It is a port of llama.cpp's common/jinja engine.

dart pub add dinja

Render a chat template

import 'package:dinja/dinja.dart';

// A trimmed Qwen2.5-style ChatML template.
const chatTemplate = r'''
{%- if tools %}
    {{- '<|im_start|>system\n' + messages[0].content + '\n\n# Tools\n\n<tools>' }}
    {%- for tool in tools %}
        {{- '\n' + tool | tojson }}
    {%- endfor %}
    {{- '\n</tools><|im_end|>\n' }}
{%- else %}
    {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
{%- endif %}
{%- for message in messages[1:] %}
    {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' }}
{%- endfor %}
{%- if add_generation_prompt %}
    {{- '<|im_start|>assistant\n' }}
{%- endif %}
''';

void main() {
  final prompt = Template(chatTemplate).render({
    'messages': [
      {'role': 'system', 'content': 'You are a helpful assistant.'},
      {'role': 'user', 'content': 'What is the weather in Paris?'},
    ],
    'tools': [
      {
        'type': 'function',
        'function': {
          'name': 'get_weather',
          'parameters': {
            'type': 'object',
            'properties': {
              'city': {'type': 'string'},
            },
          },
        },
      },
    ],
    'add_generation_prompt': true,
  });
  print(prompt);
}

Output, identical to what llama.cpp and Python Jinja2 render:

<|im_start|>system
You are a helpful assistant.

# Tools

<tools>
{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}
</tools><|im_end|>
<|im_start|>user
What is the weather in Paris?<|im_end|>
<|im_start|>assistant

Real templates come from the chat_template field of a model's tokenizer_config.json on huggingface.co, or from the tokenizer.chat_template key in GGUF metadata. Pass that string to Template unchanged.

Why dinja

  • llama.cpp parity. dinja follows common/jinja, the engine llama-server renders chat templates with. llama.cpp's Jinja test suite, 290 cases whose expected output also matches Python Jinja2 3.1.6, is ported case for case: dinja matches all 279 cases that llama.cpp runs byte for byte. The other 11 are cases llama.cpp skips as not implemented.
  • Real templates. The tests parse and render 45 distinct chat templates from real models, including Llama 3.x, Qwen2.5, Qwen3, Mistral, Gemma, DeepSeek R1, Phi, gpt-oss, Kimi K2 and GLM. With these and the templates in llama.cpp's models/templates, 86 distinct in all, dinja gives the same output as llama.cpp 7fe450e1 byte for byte, or raises the same template error, in four conversations: system prompt with tools, user only, multi-turn, and a tool call with its result.
  • Input marking. Values wrapped in JinjaString.user are escaped on output, and renderJinjaResult reports which parts of the output came from input.
  • Web and Wasm. Pure Dart, depending only on meta. The parser, runtime and llama.cpp tests also pass in Chrome, compiled to JavaScript and to Wasm.
  • Used by llamadart, a llama.cpp runtime for Dart and Flutter, to render chat templates.

Input marking

Wrap untrusted values in JinjaString.user. They are escaped when rendered; a plain String is treated as template text and is not.

Input text keeps its marking through filters, ~, loops, macros and {% set %} and {% filter %} blocks, and is escaped once, when the template outputs it. So a filter sees it as it was passed in: {% filter length %}{{ name }}{% endfilter %} counts the characters of name, not of its escaped form. tojson and join escape only the input text in their output, not the JSON's quotes or the template's separator. | safe and markSafe() turn escaping off for a value passed to render, or returned by a function passed to it. The output of a block set, macro, caller() or {% filter %} block is final, as in Jinja2 with autoescape, so | safe on it escapes the input in it.

A function passed to render is trusted: | safe on a string it builds outputs it unescaped, even when it built it from captured output. With x = JinjaString.user('<b>') and up returning its argument upper-cased, {% set c %}<i>{{ x }}{% endset %}{{ up(c)|safe }} gives <I><B>.

import 'package:dinja/dinja.dart';

void main() {
  final template = Template('Hello {{ name }}');
  const html = '<script>alert(1)</script>';
  print(template.render({'name': JinjaString.user(html)}));
  print(template.render({'name': html}));

  final result = template.renderJinjaResult({'name': JinjaString.user('Bob')});
  print(result.parts.where((part) => part.isInput).map((part) => part.val));
}
Hello &lt;script&gt;alert(1)&lt;/script&gt;
Hello <script>alert(1)</script>
(Bob)

Analyzing templates

package:dinja/ast.dart parses a template into an AST without rendering it, for tools that inspect templates, for example to detect which features a chat template uses.

import 'package:dinja/ast.dart';

void main() {
  final program = parseTemplate(
    '{% if tools %}{{ tools | length }}{% endif %}',
  );
  for (final statement in program.body) {
    print(statement.type);
  }
}
If

dinja and package:jinja

jinja ports Jinja as a general-purpose, server-side template engine with template inheritance. dinja targets LLM chat templates: it tracks llama.cpp's engine and output, adds input marking, and leaves out inheritance and template loading.

Scope

dinja implements the Jinja that chat templates use, not all of Jinja2:

  • A template is a single string: extends, block, include, import, raw and with throw a ParserException.
  • Plain strings are never escaped; only JinjaString.user values are.
  • Some Jinja2 features llama.cpp lacks are missing here too; for example, '%s'|format(x) returns %s.
  • strftime_now formats the system clock's current time in the local time zone, as C strftime does in llama.cpp. %Z is Dart's DateTime.timeZoneName, whose format depends on the platform: on the web it is the browser's name for the zone, such as Eastern Daylight Time where llama.cpp gives EDT.

Libraries

ast
Parses Jinja templates into an AST for inspection without rendering them.
dinja
A Jinja engine for LLM chat templates, ported from llama.cpp's common/jinja.