Skip to main content

xz_provider/types/
message.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4use super::cache::CacheControl;
5use super::tool::ToolCall;
6
7// ── Message ──
8
9/// 消息 —— 对话中的单条消息
10///
11/// 使用 enum 而非 flat struct,让非法状态不可表示:
12/// - 只有 Assistant 可以携带 tool_calls
13/// - 只有 Tool 必须携带 tool_call_id
14/// - 编译时就能发现角色与字段的错误组合
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "role")]
17pub enum Message {
18    #[serde(rename = "system")]
19    System {
20        content: MessageContent,
21        #[serde(skip_serializing_if = "Option::is_none")]
22        cache_control: Option<CacheControl>,
23    },
24
25    #[serde(rename = "developer")]
26    Developer {
27        content: MessageContent,
28        #[serde(skip_serializing_if = "Option::is_none")]
29        cache_control: Option<CacheControl>,
30    },
31
32    #[serde(rename = "user")]
33    User { content: MessageContent },
34
35    #[serde(rename = "assistant")]
36    Assistant {
37        /// 文本内容(可能为空,如 LLM 仅发起 tool call)
38        content: MessageContent,
39        /// LLM 发起的工具调用
40        #[serde(skip_serializing_if = "Option::is_none")]
41        tool_calls: Option<Vec<ToolCall>>,
42        /// 缓存控制标记
43        #[serde(skip_serializing_if = "Option::is_none")]
44        cache_control: Option<CacheControl>,
45        /// 推理/思考过程(DeepSeek reasoning_content, 通义千问等)
46        #[serde(rename = "reasoning_content", skip_serializing_if = "Option::is_none")]
47        reasoning_content: Option<String>,
48    },
49
50    #[serde(rename = "tool")]
51    Tool {
52        content: MessageContent,
53        tool_call_id: String,
54        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
55        is_error: bool,
56    },
57}
58
59impl Message {
60    pub fn system(text: &str) -> Self {
61        Message::System { content: MessageContent::Text(text.into()), cache_control: None }
62    }
63
64    pub fn developer(text: &str) -> Self {
65        Message::Developer { content: MessageContent::Text(text.into()), cache_control: None }
66    }
67
68    pub fn user(text: &str) -> Self {
69        Message::User { content: MessageContent::Text(text.into()) }
70    }
71
72    pub fn assistant(text: &str) -> Self {
73        Message::Assistant {
74            content: MessageContent::Text(text.into()),
75            tool_calls: None,
76            cache_control: None,
77            reasoning_content: None,
78        }
79    }
80
81    pub fn tool_result(tool_call_id: &str, content: &str) -> Self {
82        Message::Tool {
83            content: MessageContent::Text(content.into()),
84            tool_call_id: tool_call_id.into(),
85            is_error: false,
86        }
87    }
88
89    pub fn tool_error(tool_call_id: &str, error: &str) -> Self {
90        Message::Tool {
91            content: MessageContent::Text(error.into()),
92            tool_call_id: tool_call_id.into(),
93            is_error: true,
94        }
95    }
96
97    /// 返回消息的角色标识字符串
98    pub fn role_str(&self) -> &'static str {
99        match self {
100            Message::System { .. } => "system",
101            Message::Developer { .. } => "developer",
102            Message::User { .. } => "user",
103            Message::Assistant { .. } => "assistant",
104            Message::Tool { .. } => "tool",
105        }
106    }
107}
108
109impl fmt::Display for Message {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Message::System { content, .. }
113            | Message::Developer { content, .. }
114            | Message::User { content, .. }
115            | Message::Assistant { content, .. }
116            | Message::Tool { content, .. } => match content {
117                MessageContent::Text(t) => write!(f, "{}", t),
118                MessageContent::MultiPart(parts) => {
119                    for part in parts {
120                        match part {
121                            ContentPart::Text { text, .. } => write!(f, "{}", text)?,
122                            ContentPart::ImageUrl { .. } => write!(f, "[Image]")?,
123                            ContentPart::ImageBase64 { .. } => write!(f, "[Image(base64)]")?,
124                            ContentPart::AudioBase64 { .. } => write!(f, "[Audio]")?,
125                            ContentPart::File { filename, .. } => {
126                                if let Some(name) = filename {
127                                    write!(f, "[File: {}]", name)?
128                                } else {
129                                    write!(f, "[File]")?
130                                }
131                            }
132                            ContentPart::Document { title, .. } => {
133                                if let Some(t) = title {
134                                    write!(f, "[Document: {}]", t)?
135                                } else {
136                                    write!(f, "[Document]")?
137                                }
138                            }
139                            ContentPart::RedactedThinking { .. } => write!(f, "[Thinking]")?,
140                            ContentPart::ToolReference { tool_name, .. } => {
141                                write!(f, "[Tool: {}]", tool_name)?
142                            }
143                        }
144                    }
145                    Ok(())
146                }
147                MessageContent::None => Ok(()),
148            },
149        }
150    }
151}
152
153// ── MessageContent ──
154
155/// 消息内容
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(untagged)]
158pub enum MessageContent {
159    /// 纯文本
160    Text(String),
161    /// 多模态内容(文本 + 图片 + 文件)
162    MultiPart(Vec<ContentPart>),
163    /// 空内容(Assistant 仅发起 tool call 时)
164    None,
165}
166
167impl From<String> for MessageContent {
168    fn from(s: String) -> Self {
169        MessageContent::Text(s)
170    }
171}
172
173impl From<&str> for MessageContent {
174    fn from(s: &str) -> Self {
175        MessageContent::Text(s.to_owned())
176    }
177}
178
179impl From<Vec<ContentPart>> for MessageContent {
180    fn from(parts: Vec<ContentPart>) -> Self {
181        MessageContent::MultiPart(parts)
182    }
183}
184
185// ── DocumentSource ──
186
187/// 文档来源
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(tag = "type")]
190pub enum DocumentSource {
191    #[serde(rename = "base64")]
192    Base64 { media_type: String, data: String },
193    #[serde(rename = "url")]
194    Url { url: String },
195}
196
197// ── ContentPart ──
198
199/// 多模态内容片段
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(tag = "type")]
202pub enum ContentPart {
203    #[serde(rename = "text")]
204    Text { text: String },
205
206    #[serde(rename = "image_url")]
207    ImageUrl {
208        url: String,
209        #[serde(skip_serializing_if = "Option::is_none")]
210        detail: Option<ImageDetail>,
211    },
212
213    #[serde(rename = "image_base64")]
214    ImageBase64 { media_type: String, data: String },
215
216    /// 音频输入(GPT-4o-audio、Gemini Audio 等多模态音频模型)
217    #[serde(rename = "audio_base64")]
218    AudioBase64 { media_type: String, data: String },
219
220    /// 文件/文档引用 — 轻量引用,仅携带 file_id
221    /// 文件上传/管理由独立 crate(如 xz-upload)负责,不混入 provider 层
222    #[serde(rename = "file")]
223    File {
224        file_id: String,
225        #[serde(skip_serializing_if = "Option::is_none")]
226        filename: Option<String>,
227    },
228
229    /// 文档内容(内嵌 base64 或 URL 引用)
230    #[serde(rename = "document")]
231    Document {
232        source: DocumentSource,
233        #[serde(skip_serializing_if = "Option::is_none")]
234        title: Option<String>,
235        #[serde(skip_serializing_if = "Option::is_none")]
236        cache_control: Option<CacheControl>,
237    },
238
239    /// 思考过程(已脱敏,仅含 token 计数等元信息)
240    #[serde(rename = "redacted_thinking")]
241    RedactedThinking { data: String },
242
243    /// 工具引用(多工具场景下标识正在调用的工具源)
244    #[serde(rename = "tool_reference")]
245    ToolReference {
246        tool_name: String,
247        #[serde(skip_serializing_if = "Option::is_none")]
248        cache_control: Option<CacheControl>,
249    },
250}
251
252// ── ImageDetail ──
253
254/// 图片细节级别
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub enum ImageDetail {
257    #[serde(rename = "auto")]
258    Auto,
259    #[serde(rename = "low")]
260    Low,
261    #[serde(rename = "high")]
262    High,
263}
264
265// ── Deprecated/Compat ──
266
267/// 兼容旧的 Role 枚举(v1 代码过渡使用)
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
269#[serde(rename_all = "snake_case")]
270pub enum Role {
271    System,
272    User,
273    Assistant,
274    Tool,
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_system_message() {
283        let msg = Message::system("You are a helpful assistant.");
284        assert_eq!(msg.role_str(), "system");
285        match msg {
286            Message::System { content, cache_control } => {
287                assert!(matches!(content, MessageContent::Text(_)));
288                assert!(cache_control.is_none());
289            }
290            _ => panic!("Expected System variant"),
291        }
292    }
293
294    #[test]
295    fn test_user_message() {
296        let msg = Message::user("Hello!");
297        assert_eq!(msg.role_str(), "user");
298        match msg {
299            Message::User { content } => {
300                assert!(matches!(content, MessageContent::Text(_)));
301            }
302            _ => panic!("Expected User variant"),
303        }
304    }
305
306    #[test]
307    fn test_assistant_message() {
308        let msg = Message::assistant("Hi!");
309        assert_eq!(msg.role_str(), "assistant");
310        match msg {
311            Message::Assistant { content, tool_calls, cache_control, .. } => {
312                assert!(matches!(content, MessageContent::Text(_)));
313                assert!(tool_calls.is_none());
314                assert!(cache_control.is_none());
315            }
316            _ => panic!("Expected Assistant variant"),
317        }
318    }
319
320    #[test]
321    fn test_tool_result() {
322        let msg = Message::tool_result("call_1", "result data");
323        assert_eq!(msg.role_str(), "tool");
324        match msg {
325            Message::Tool { content, tool_call_id, is_error } => {
326                assert!(matches!(content, MessageContent::Text(_)));
327                assert_eq!(tool_call_id, "call_1");
328                assert!(!is_error);
329            }
330            _ => panic!("Expected Tool variant"),
331        }
332    }
333
334    #[test]
335    fn test_tool_error() {
336        let msg = Message::tool_error("call_1", "error!");
337        match msg {
338            Message::Tool { ref tool_call_id, is_error, .. } => {
339                assert!(is_error);
340                assert_eq!(tool_call_id, "call_1");
341            }
342            _ => panic!("Expected Tool variant"),
343        }
344    }
345
346    #[test]
347    fn test_role_str_all_variants() {
348        assert_eq!(Message::system("a").role_str(), "system");
349        assert_eq!(Message::user("a").role_str(), "user");
350        assert_eq!(Message::assistant("a").role_str(), "assistant");
351        assert_eq!(Message::tool_result("1", "a").role_str(), "tool");
352    }
353
354    #[test]
355    fn test_message_content_from_string() {
356        let content: MessageContent = "hello".to_string().into();
357        assert!(matches!(content, MessageContent::Text(_)));
358    }
359
360    #[test]
361    fn test_message_content_from_str() {
362        let content: MessageContent = "hello".into();
363        assert!(matches!(content, MessageContent::Text(_)));
364    }
365
366    #[test]
367    fn test_message_content_from_vec_parts() {
368        let parts = vec![ContentPart::Text { text: "hello".into() }];
369        let content: MessageContent = parts.into();
370        assert!(matches!(content, MessageContent::MultiPart(_)));
371    }
372
373    #[test]
374    fn test_message_display_text() {
375        let msg = Message::user("Hello!");
376        assert_eq!(format!("{}", msg), "Hello!");
377    }
378
379    #[test]
380    fn test_message_display_multipart() {
381        let parts = vec![
382            ContentPart::Text { text: "Check ".into() },
383            ContentPart::ImageUrl { url: "https://example.com/img.png".into(), detail: None },
384        ];
385        let content = MessageContent::MultiPart(parts);
386        let msg = Message::User { content };
387        assert_eq!(format!("{}", msg), "Check [Image]");
388    }
389
390    #[test]
391    fn test_message_display_none() {
392        let msg = Message::Assistant {
393            content: MessageContent::None,
394            tool_calls: None,
395            cache_control: None,
396            reasoning_content: None,
397        };
398        assert_eq!(format!("{}", msg), "");
399    }
400
401    #[test]
402    fn test_content_part_image_base64_display() {
403        let part =
404            ContentPart::ImageBase64 { media_type: "image/png".into(), data: "base64data".into() };
405        let msg = Message::User { content: MessageContent::MultiPart(vec![part]) };
406        assert_eq!(format!("{}", msg), "[Image(base64)]");
407    }
408
409    #[test]
410    fn test_message_serde_roundtrip() {
411        let msg = Message::assistant("Hello");
412        let json = serde_json::to_string(&msg).unwrap();
413        let deserialized: Message = serde_json::from_str(&json).unwrap();
414        assert_eq!(msg.role_str(), deserialized.role_str());
415    }
416
417    #[test]
418    fn test_image_detail_serde() {
419        let json = r#""auto""#;
420        let detail: ImageDetail = serde_json::from_str(json).unwrap();
421        assert!(matches!(detail, ImageDetail::Auto));
422    }
423
424    #[test]
425    fn test_content_part_serde() {
426        let part = ContentPart::Text { text: "hello".into() };
427        let json = serde_json::to_string(&part).unwrap();
428        let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
429        match deserialized {
430            ContentPart::Text { text } => assert_eq!(text, "hello"),
431            _ => panic!("Expected Text variant"),
432        }
433    }
434
435    // ── Developer tests ──
436
437    #[test]
438    fn test_developer_message() {
439        let msg = Message::developer("You are a code expert.");
440        assert_eq!(msg.role_str(), "developer");
441        match msg {
442            Message::Developer { content, cache_control } => {
443                assert!(matches!(content, MessageContent::Text(_)));
444                assert!(cache_control.is_none());
445            }
446            _ => panic!("Expected Developer variant"),
447        }
448    }
449
450    #[test]
451    fn test_developer_serde_roundtrip() {
452        let msg = Message::developer("You are a Python expert.");
453        let json = serde_json::to_string(&msg).unwrap();
454        assert!(json.contains(r#""role":"developer""#));
455        let deserialized: Message = serde_json::from_str(&json).unwrap();
456        assert!(matches!(deserialized, Message::Developer { .. }));
457    }
458
459    // ── DocumentSource tests ──
460
461    #[test]
462    fn test_document_source_base64_serde() {
463        let src = DocumentSource::Base64 {
464            media_type: "application/pdf".into(),
465            data: "base64data".into(),
466        };
467        let json = serde_json::to_string(&src).unwrap();
468        assert!(json.contains(r#""type":"base64""#));
469        let deserialized: DocumentSource = serde_json::from_str(&json).unwrap();
470        match deserialized {
471            DocumentSource::Base64 { media_type, data } => {
472                assert_eq!(media_type, "application/pdf");
473                assert_eq!(data, "base64data");
474            }
475            _ => panic!("Expected Base64 variant"),
476        }
477    }
478
479    #[test]
480    fn test_document_source_url_serde() {
481        let src = DocumentSource::Url { url: "https://example.com/doc.pdf".into() };
482        let json = serde_json::to_string(&src).unwrap();
483        assert!(json.contains(r#""type":"url""#));
484        let deserialized: DocumentSource = serde_json::from_str(&json).unwrap();
485        match deserialized {
486            DocumentSource::Url { url } => assert_eq!(url, "https://example.com/doc.pdf"),
487            _ => panic!("Expected Url variant"),
488        }
489    }
490
491    // ── ContentPart new variant tests ──
492
493    #[test]
494    fn test_content_part_document_serde() {
495        let part = ContentPart::Document {
496            source: DocumentSource::Base64 {
497                media_type: "text/plain".into(),
498                data: "hello".into(),
499            },
500            title: Some("readme".into()),
501            cache_control: None,
502        };
503        let json = serde_json::to_string(&part).unwrap();
504        assert!(json.contains(r#""type":"document""#));
505        let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
506        match deserialized {
507            ContentPart::Document { source, title, .. } => {
508                assert!(matches!(source, DocumentSource::Base64 { .. }));
509                assert_eq!(title, Some("readme".into()));
510            }
511            _ => panic!("Expected Document variant"),
512        }
513    }
514
515    #[test]
516    fn test_content_part_redacted_thinking_serde() {
517        let part = ContentPart::RedactedThinking { data: "redacted_token_count".into() };
518        let json = serde_json::to_string(&part).unwrap();
519        assert!(json.contains(r#""type":"redacted_thinking""#));
520        let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
521        match deserialized {
522            ContentPart::RedactedThinking { data } => assert_eq!(data, "redacted_token_count"),
523            _ => panic!("Expected RedactedThinking variant"),
524        }
525    }
526
527    #[test]
528    fn test_content_part_tool_reference_serde() {
529        let part =
530            ContentPart::ToolReference { tool_name: "get_weather".into(), cache_control: None };
531        let json = serde_json::to_string(&part).unwrap();
532        assert!(json.contains(r#""type":"tool_reference""#));
533        let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
534        match deserialized {
535            ContentPart::ToolReference { tool_name, .. } => assert_eq!(tool_name, "get_weather"),
536            _ => panic!("Expected ToolReference variant"),
537        }
538    }
539}