Skip to main content

hf_chat_template/
error.rs

1//! Error model. One enum; template-raised rejections are distinguished from engine bugs so
2//! callers can act on them (reject the conversation) vs. log a library issue.
3
4use std::fmt;
5
6/// Private marker prefixed onto messages passed to `raise_exception` so we can reliably
7/// recover them from minijinja's error chain without shared mutable state. Never appears in
8/// successful render output (a raise always produces an error, not text).
9pub(crate) const RAISE_SENTINEL: &str = "\u{0}HFCT_RAISE\u{1}";
10
11/// Errors produced while compiling or rendering a chat template.
12#[derive(Debug)]
13#[non_exhaustive]
14pub enum Error {
15    /// The template deliberately called `raise_exception(msg)` — the input conversation was
16    /// rejected by the template's own validation (e.g. bad role alternation). Act on this;
17    /// it is not a bug.
18    TemplateRaised {
19        /// The message the template passed to `raise_exception`.
20        message: String,
21    },
22    /// The Jinja source failed to compile (syntax error). Carries the underlying engine error.
23    Compile(minijinja::Error),
24    /// The template failed at render time (undefined variable, type error, unknown method…).
25    Render(minijinja::Error),
26    /// The `tokenizer_config.json` had no usable `chat_template`, or a requested named
27    /// template was absent / the field had an unexpected shape.
28    Config(String),
29    /// Fetching a file from the Hugging Face Hub failed (network, auth/gated repo, or the file
30    /// is absent). Only present with the `hub` feature. Carries the underlying message; the
31    /// `hf-hub` error type is deliberately kept out of our public API.
32    #[cfg(feature = "hub")]
33    Hub(String),
34}
35
36impl fmt::Display for Error {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Error::TemplateRaised { message } => {
40                write!(f, "template rejected the input: {message}")
41            }
42            Error::Compile(e) => write!(f, "chat template failed to compile: {e}"),
43            Error::Render(e) => write!(f, "chat template failed to render: {e}"),
44            Error::Config(m) => write!(f, "invalid chat-template config: {m}"),
45            #[cfg(feature = "hub")]
46            Error::Hub(m) => write!(f, "failed to fetch from the Hugging Face Hub: {m}"),
47        }
48    }
49}
50
51impl std::error::Error for Error {
52    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        match self {
54            Error::Compile(e) | Error::Render(e) => Some(e),
55            _ => None,
56        }
57    }
58}
59
60impl Error {
61    /// Map a minijinja render-time error into our error model, recovering a template-raised
62    /// message (marked with [`RAISE_SENTINEL`]) into [`Error::TemplateRaised`].
63    pub(crate) fn from_render(e: minijinja::Error) -> Self {
64        // Walk the message and the error source chain looking for our sentinel.
65        if let Some(msg) = find_raised_message(&e) {
66            return Error::TemplateRaised { message: msg };
67        }
68        Error::Render(e)
69    }
70}
71
72/// Scan a minijinja error (and its source chain) for a sentinel-marked raise message.
73fn find_raised_message(e: &minijinja::Error) -> Option<String> {
74    let mut current: Option<&(dyn std::error::Error + 'static)> = Some(e);
75    while let Some(err) = current {
76        let s = err.to_string();
77        if let Some(idx) = s.find(RAISE_SENTINEL) {
78            let after = &s[idx + RAISE_SENTINEL.len()..];
79            // The raised message runs to the next sentinel (if minijinja appended context)
80            // or to end of string.
81            let msg = match after.find(RAISE_SENTINEL) {
82                Some(end) => &after[..end],
83                None => after,
84            };
85            return Some(msg.to_string());
86        }
87        current = err.source();
88    }
89    None
90}