1use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct ToolCallMessage {
10 pub id: String,
11 pub name: String,
12 pub arguments: String,
13}
14
15#[derive(Clone, Debug, Serialize, Deserialize)]
17pub enum ImageAttachment {
18 Url {
19 url: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 detail: Option<ImageDetail>,
22 },
23 Base64 {
24 data: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 media_type: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 detail: Option<ImageDetail>,
29 },
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
34pub enum ImageDetail {
35 Low,
36 High,
37 Auto,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
45pub enum ChatMessage {
46 System {
47 content: String,
48 #[serde(default, skip_serializing)]
50 ephemeral: bool,
51 },
52 User {
53 content: String,
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 images: Vec<ImageAttachment>,
56 #[serde(default, skip_serializing)]
58 ephemeral: bool,
59 },
60 Assistant {
61 content: Option<String>,
62 reasoning_content: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
66 thinking_signature: Option<String>,
67 tool_calls: Option<Vec<ToolCallMessage>>,
68 },
69 Tool {
70 tool_call_id: String,
71 name: Option<String>,
72 content: String,
73 },
74 Custom {
79 role: String,
80 data: serde_json::Value,
81 },
82}
83
84impl ChatMessage {
85 pub fn system(content: impl Into<String>) -> Self {
86 Self::System {
87 content: content.into(),
88 ephemeral: false,
89 }
90 }
91
92 pub fn system_ephemeral(content: impl Into<String>) -> Self {
94 Self::System {
95 content: content.into(),
96 ephemeral: true,
97 }
98 }
99
100 pub fn user(content: impl Into<String>) -> Self {
101 Self::User {
102 content: content.into(),
103 images: Vec::new(),
104 ephemeral: false,
105 }
106 }
107
108 pub fn user_ephemeral(content: impl Into<String>) -> Self {
110 Self::User {
111 content: content.into(),
112 images: Vec::new(),
113 ephemeral: true,
114 }
115 }
116
117 pub fn user_with_images(content: impl Into<String>, images: Vec<ImageAttachment>) -> Self {
118 Self::User {
119 content: content.into(),
120 images,
121 ephemeral: false,
122 }
123 }
124
125 pub fn is_ephemeral(&self) -> bool {
127 match self {
128 Self::System { ephemeral, .. } => *ephemeral,
129 Self::User { ephemeral, .. } => *ephemeral,
130 _ => false,
131 }
132 }
133
134 pub fn assistant(content: impl Into<String>) -> Self {
135 Self::Assistant {
136 content: Some(content.into()),
137 reasoning_content: None,
138 thinking_signature: None,
139 tool_calls: None,
140 }
141 }
142
143 pub fn assistant_with_reasoning(
144 content: impl Into<String>,
145 reasoning: impl Into<String>,
146 ) -> Self {
147 Self::Assistant {
148 content: Some(content.into()),
149 reasoning_content: Some(reasoning.into()),
150 thinking_signature: None,
151 tool_calls: None,
152 }
153 }
154
155 pub fn assistant_tool_call(
156 tool_call_id: impl Into<String>,
157 tool_name: impl Into<String>,
158 arguments: impl Into<String>,
159 ) -> Self {
160 Self::Assistant {
161 content: None,
162 reasoning_content: None,
163 thinking_signature: None,
164 tool_calls: Some(vec![ToolCallMessage {
165 id: tool_call_id.into(),
166 name: tool_name.into(),
167 arguments: arguments.into(),
168 }]),
169 }
170 }
171
172 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
173 Self::Tool {
174 tool_call_id: tool_call_id.into(),
175 name: None,
176 content: content.into(),
177 }
178 }
179
180 pub fn tool_with_name(
181 tool_call_id: impl Into<String>,
182 name: impl Into<String>,
183 content: impl Into<String>,
184 ) -> Self {
185 Self::Tool {
186 tool_call_id: tool_call_id.into(),
187 name: Some(name.into()),
188 content: content.into(),
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn chat_message_user() {
199 let msg = ChatMessage::user("hello");
200 match &msg {
201 ChatMessage::User {
202 content,
203 images,
204 ephemeral,
205 } => {
206 assert_eq!(content, "hello");
207 assert!(images.is_empty());
208 assert!(!ephemeral);
209 }
210 _ => panic!("Expected User variant"),
211 }
212 }
213
214 #[test]
215 fn chat_message_system() {
216 let msg = ChatMessage::system("sys");
217 assert!(!msg.is_ephemeral());
218 }
219
220 #[test]
221 fn chat_message_system_ephemeral() {
222 let msg = ChatMessage::system_ephemeral("sys");
223 assert!(msg.is_ephemeral());
224 }
225
226 #[test]
227 fn chat_message_user_ephemeral() {
228 let msg = ChatMessage::user_ephemeral("hi");
229 assert!(msg.is_ephemeral());
230 }
231
232 #[test]
233 fn chat_message_assistant() {
234 let msg = ChatMessage::assistant("response");
235 match &msg {
236 ChatMessage::Assistant { content, .. } => {
237 assert_eq!(content.as_deref(), Some("response"));
238 }
239 _ => panic!("Expected Assistant variant"),
240 }
241 }
242
243 #[test]
244 fn chat_message_tool_call() {
245 let msg = ChatMessage::assistant_tool_call("id1", "echo", r#"{"x":1}"#);
246 match &msg {
247 ChatMessage::Assistant { tool_calls, .. } => {
248 let tc = tool_calls.as_ref().unwrap();
249 assert_eq!(tc.len(), 1);
250 assert_eq!(tc[0].id, "id1");
251 assert_eq!(tc[0].name, "echo");
252 }
253 _ => panic!("Expected Assistant variant"),
254 }
255 }
256
257 #[test]
258 fn chat_message_tool() {
259 let msg = ChatMessage::tool("id1", "result");
260 match &msg {
261 ChatMessage::Tool {
262 tool_call_id,
263 name,
264 content,
265 } => {
266 assert_eq!(tool_call_id, "id1");
267 assert!(name.is_none());
268 assert_eq!(content, "result");
269 }
270 _ => panic!("Expected Tool variant"),
271 }
272 }
273
274 #[test]
275 fn chat_message_tool_with_name() {
276 let msg = ChatMessage::tool_with_name("id1", "echo", "result");
277 match &msg {
278 ChatMessage::Tool {
279 tool_call_id,
280 name,
281 content,
282 } => {
283 assert_eq!(tool_call_id, "id1");
284 assert_eq!(name.as_deref(), Some("echo"));
285 assert_eq!(content, "result");
286 }
287 _ => panic!("Expected Tool variant"),
288 }
289 }
290
291 #[test]
292 fn custom_message_not_ephemeral() {
293 let msg = ChatMessage::Custom {
294 role: "artifact".to_string(),
295 data: serde_json::json!({}),
296 };
297 assert!(!msg.is_ephemeral());
298 }
299
300 #[test]
301 fn serialization_roundtrip() {
302 let msg = ChatMessage::user("hello");
303 let json = serde_json::to_string(&msg).unwrap();
304 let deserialized: ChatMessage = serde_json::from_str(&json).unwrap();
305 match deserialized {
306 ChatMessage::User { content, .. } => assert_eq!(content, "hello"),
307 _ => panic!("Expected User"),
308 }
309 }
310}