Skip to main content

hf_chat_template/
model.rs

1//! Typed input model — the data you hand to [`ChatTemplate::render`](crate::ChatTemplate::render).
2//!
3//! These mirror the OpenAI/HF message shape and are fully `serde`-(de)serializable, so callers
4//! can deserialize their existing request JSON straight in. The model is deliberately *thin*:
5//! the stable parts (`role`) are typed, while everything genuinely open-ended — tool schemas,
6//! tool-call payloads, multimodal content parts, and per-model extra kwargs — stays as
7//! [`serde_json::Value`] so we never lose a field or impose a shape a template doesn't expect.
8//!
9//! Key order is load-bearing (it must survive into `| tojson`); this is why the crate enables
10//! `serde_json`'s `preserve_order` feature. See `src/json.rs`.
11
12use serde::{Deserialize, Serialize};
13use serde_json::{Map, Value as Json};
14
15/// Everything a chat template renders from: the conversation plus optional tools/documents
16/// and the generation-prompt flag. Unmodeled keys land in [`extra`](RenderInput::extra) and
17/// are passed to the Jinja context verbatim.
18#[derive(Clone, Debug, Default, Serialize, Deserialize)]
19pub struct RenderInput {
20    /// The conversation, in order.
21    pub messages: Vec<Message>,
22
23    /// Tool/function definitions (JSON-schema), serialized by templates via `tools | tojson`.
24    /// Left as raw JSON because the schema shape is open-ended and key order matters.
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub tools: Vec<Json>,
27
28    /// RAG documents for grounded-generation templates (e.g. Command-R).
29    #[serde(default, skip_serializing_if = "Vec::is_empty")]
30    pub documents: Vec<Json>,
31
32    /// Whether to append the assistant generation prefix. The template branches on this;
33    /// we never synthesize the prefix ourselves.
34    #[serde(default)]
35    pub add_generation_prompt: bool,
36
37    /// Arbitrary extra template kwargs some models read (`enable_thinking`, `builtin_tools`,
38    /// …). Flattened into the top-level Jinja context. Order-preserving.
39    #[serde(default, flatten)]
40    pub extra: Map<String, Json>,
41}
42
43/// A single chat message. `role` is typed; `content` is string-or-parts; all other keys
44/// (`name`, `tool_call_id`, …) flow through [`extra`](Message::extra).
45#[derive(Clone, Debug, Default, Serialize, Deserialize)]
46pub struct Message {
47    /// `"system" | "user" | "assistant" | "tool" | …` — open by design.
48    pub role: String,
49
50    /// Message body: a plain string, or a list of multimodal parts. `None` omits the key
51    /// entirely (faithful to messages that carry only `tool_calls`).
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub content: Option<Content>,
54
55    /// Assistant tool-call payloads, raw JSON (shape varies by model:
56    /// `function.arguments` is sometimes a dict, sometimes a JSON string).
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub tool_calls: Vec<Json>,
59
60    /// Any other per-message keys (`name`, `tool_call_id`, model-specific fields).
61    #[serde(default, flatten)]
62    pub extra: Map<String, Json>,
63}
64
65impl Message {
66    /// Convenience constructor for the common `{role, content: <text>}` message.
67    pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
68        Message {
69            role: role.into(),
70            content: Some(Content::Text(content.into())),
71            tool_calls: Vec::new(),
72            extra: Map::new(),
73        }
74    }
75
76    /// A `system` message with text content. Shorthand for `Message::new("system", text)`.
77    pub fn system(content: impl Into<String>) -> Self {
78        Message::new("system", content)
79    }
80
81    /// A `user` message with text content. Shorthand for `Message::new("user", text)`.
82    pub fn user(content: impl Into<String>) -> Self {
83        Message::new("user", content)
84    }
85
86    /// An `assistant` message with text content. Shorthand for `Message::new("assistant", text)`.
87    pub fn assistant(content: impl Into<String>) -> Self {
88        Message::new("assistant", content)
89    }
90}
91
92/// Message content: a plain string, or a list of multimodal parts.
93///
94/// Serialized untagged, so `Text` becomes a JSON string and `Parts` a JSON array — preserving
95/// the distinction templates probe with `content is string`.
96#[derive(Clone, Debug, Serialize, Deserialize)]
97#[serde(untagged)]
98pub enum Content {
99    /// A plain text body.
100    Text(String),
101    /// `[{ "type": "text", "text": … }, { "type": "image", … }, …]` — raw JSON parts.
102    Parts(Vec<Json>),
103}
104
105impl Default for Content {
106    fn default() -> Self {
107        Content::Text(String::new())
108    }
109}