Skip to content

Repository files navigation

hfjinja

hfjinja is a dependency-free Java 21+ implementation of the Hugging Face chat-template Jinja subset. It evaluates a model's tokenizer_config.json chat_template without a JavaScript engine, with compatibility pinned to a reviewed version of @huggingface/jinja.

It is designed for JVM model clients that need reproducible rendering of Llama, Qwen, Mistral, and tool-use templates—not as a general-purpose or Python-compatible Jinja2 engine.

Building the project requires a locally installed JDK 21 available through JAVA_HOME or PATH. Running ./gradlew check also requires exactly Node.js 26.7.0, the pinned oracle version.

Status: the repository is under active implementation. The intended v1 behavior and public API are described below; see the implementation plan for progress.

Features

  • Native Java 21 implementation with no runtime dependencies, Node.js, JNI, or JS engine.
  • Immutable parsed templates that are safe to render concurrently.
  • Hugging Face Jinja syntax and runtime semantics: expressions, control flow, loops, filters, tests, macros, call/filter blocks, slices, keyword/spread arguments, tojson, and raise_exception.
  • Canonical formatting of parsed templates through Template.format(), with tab, string, or numeric-space indentation.
  • Byte-exact compatibility corpus generated from the pinned Node package, including whitespace and Unicode behavior.
  • Closed host-value boundary: templates cannot reflect over Java objects, call Java methods, or access classes.
  • Configurable parsing/rendering limits and location-aware errors.
  • Deterministic time support through an explicit Clock and ZoneId.
  • Reproducible upstream provenance: vendored upstream sources, package integrity, source hashes, mapping ledger, and offline verification.

Installation

dependencies {
    implementation("se.alipsa:hfjinja:0.5.0")
}

The library is also a JPMS module:

module example.app {
  requires se.alipsa.hfjinja;
}

Releases

The initial release will be 0.5.0. This project follows Semantic Versioning; see the changelog, which is maintained in the Keep a Changelog format, for notable changes.

Quick start

import java.util.List;
import java.util.Map;
import se.alipsa.hfjinja.Template;

var template = Template.parse("""
    {% for message in messages %}
    {{ message.role }}: {{ message.content }}
    {% endfor %}
    """);

var prompt = template.render(Map.of(
    "messages", List.of(
        Map.of("role", "user", "content", "Hello"),
        Map.of("role", "assistant", "content", "Hi!"))));

Parse once and reuse the Template from multiple threads. Both the String and Appendable overloads buffer output; the latter performs one terminal append after a successful render. An Appendable that fails while appending can still contain a partial terminal write.

template.render(context, writer);

To produce the pinned upstream's canonical template style without rendering it, call format():

String canonical = template.format();       // tab indentation
String spaces = template.format(2);         // two spaces per nesting level
String custom = template.format("  ");      // string indentation unit

Local tokenizer-config integration

See the local tokenizer-config example. It reads a local tokenizer_config.json chat template, parses it once, and renders a prompt. Production applications should use their normal JSON library to obtain that string before calling Template.parse.

Values and safety

Contexts accept strings, booleans, JSON-compatible Number values, arrays, List<?>, and Map<String, ?>. A missing map key is template undefined; a present Java null is template null. Unsupported values, non-string map keys, and cyclic graphs fail with TemplateRenderException and HOST_CONVERSION.

The number boundary follows JavaScript's number model. It accepts a finite Number when its shortest JavaScript round-trip representation preserves its canonical decimal text, and rejects unsafe integers, non-finite input, and decimal narrowing. Runtime arithmetic still follows JavaScript behavior, including non-finite computed results and precision loss.

Host functions and deterministic time

Use RenderOptions to register explicitly named host functions. A template can invoke only these registered functions and built-ins provided by the pinned upstream package—never arbitrary Java methods or reflection.

var options = RenderOptions.builder()
    .clock(Clock.fixed(Instant.parse("2025-01-02T03:04:05Z"), ZoneOffset.UTC))
    .zoneId(ZoneOffset.UTC)
    .hostFunction("format_tool", arguments -> "...")
    .build();

var output = template.render(context, options);

Time-dependent globals require an explicit clock and zone. If one is absent, rendering fails at the first such use rather than reading the host wall clock. Date formatting uses fixed C/POSIX month and weekday names rather than the host locale.

Host-function keyword arguments are not part of v1 and fail with HOST_FUNCTION. Exceptions from host functions and invalid returned values are wrapped as TemplateRenderException with the call site location.

Errors and limits

All documented failures derive from HfJinjaException:

try {
  template.render(context);
} catch (HfJinjaException error) {
  ErrorCategory category = error.category();
  error.location().ifPresent(System.err::println);
}

Categories cover syntax, undefined/access, type, arity, value, explicit raises, host functions, host conversion, resource limits, and output failures. New categories may be added in minor releases; consumers should include a default branch when switching on them.

Runtime compatibility checks compare error categories by default. The pinned Node oracle's stable unknown-filter and receiver-error diagnostic families are additionally checked exactly where documented by regression tests; other error wording remains category-only compatibility evidence. For example, 1 | upper remains category-only because the Node runtime selects an unknown numeric filter diagnostic. Known-name wrong-receiver cases such as [1] | items are likewise category-only until their behavior is individually closed.

TemplateOptions and RenderOptions configure source, token, AST-depth, render-step, loop, macro-depth, and output-size limits. Resource-limit failures are distinct from compatibility results. Appendable write failures are reported as OUTPUT errors.

Compatibility and upstream updates

hfjinja targets one explicit upstream package revision, currently @huggingface/jinja 0.5.9. Selected, policy-approved package source is committed under upstream/vendor/; upstream/upstream-lock.json records the tarball integrity, commit, Node oracle version, global inventory, source hashes, and policy exclusions.

./gradlew check

The command runs unit tests and upstreamVerify, which works without network access and fails on changed vendor hashes, stale mappings, unaccounted AST nodes, or stale no-impact reviews. Updating the pin is an explicit, reviewed sync change—not an incidental dependency upgrade.

The test build runs both the pinned Node oracle and the Java differential runner over src/test/resources/corpus/v1.jsonl; these are test-time tools only, not library runtime dependencies. Hash-only records are intentionally skipped by the normal build and require supplied external fixture material. build/reports/corpus-coverage.md lists the upstream cases currently represented by the corpus.

Successful compatibility output compares byte-for-byte against the pinned Node package. Error comparison uses documented categories only. Python transformers/Jinja2 output is not an oracle.

To deliberately refresh the reviewed template-format golden after changing its vectors or the pinned upstream, run:

node tools/format/format-golden.mjs --vectors src/test/resources/format/vectors.json --update src/test/resources/format/upstream-formatted.json
./gradlew formatGoldenVerify

Project layout

src/main/java/se/alipsa/hfjinja/  public API
src/main/java/.../internal/       lexer, parser, AST, values, interpreter
src/test/                         unit, compatibility, safety, and concurrency tests
upstream/vendor/                  selected, policy-approved @huggingface/jinja source
upstream/upstream-lock.json       provenance and global inventory
upstream/mapping.yml              source-to-Java mapping ledger
req/                              requirements and implementation plan

License

hfjinja is released under the MIT License. It retains attribution for vendored upstream MIT source in NOTICE. Retained model-template fixtures include their applicable license and notice requirements; where those terms make text vendoring unsuitable, the project uses hash-only fixtures. See the model fixture licensing policy for the approved fixture forms.

About

Java port of @huggingface/jinja

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages