dinja
Motor Jinja para modelos de conversa do Hugging Face e GGUF: mensagens, ferramentas, prompts de geração. Testado em paridade com llama.cpp e Jinja2. Todos os plataformas, incluindo web e Wasm.
Motor Jinja para modelos de conversa do Hugging Face e GGUF: mensagens, ferramentas, prompts de geração. Testado em paridade com llama.cpp e Jinja2. Todos os plataformas, incluindo web e Wasm.
Texto original em inglês. Visite o GitHub para a versão atual.
pub package pub points Build Status License: MIT codecov
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
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.
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.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.JinjaString.user are escaped on output, and renderJinjaResult reports which parts of the output came from input.meta. The parser, runtime and llama.cpp tests also pass in Chrome, compiled to JavaScript and to Wasm.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 <script>alert(1)</script>
Hello <script>alert(1)</script>
(Bob)
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
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.
dinja implements the Jinja that chat templates use, not all of Jinja2:
extends, block, include, import, raw and with throw a ParserException.JinjaString.user values are.'%s'|format(x) returns %s.\xHH with exactly two hexadecimal digits, as in {{ '\x41\xE9' }} rendering Aé. Values above 0x7f are Unicode characters, as in llama.cpp; \\x41 keeps the literal text \x41.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.
To supply a deterministic clock for one render, pass
{'strftime_now': strftimeNow(DateTime.utc(2026, 1, 2))} to render.
The callable keeps the built-in argument/output checks and input escaping;
other renders retain their own clock.