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}