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}