Skip to main content

deepseek_sdk/chat/
mod.rs

1//! Chat completion request/response models.
2//!
3//! This module contains the data structures for the `/chat/completions` API
4//! and re-exports streaming helpers from the request implementation.
5use crate::DeepSeekClient;
6use serde::{Deserialize, Serialize};
7
8pub mod request;
9pub mod response;
10pub mod stream;
11
12pub use stream::{ChatStreamBlocking, ChatStreamItem};
13
14/// Helper to skip serialization of empty `Vec` fields wrapped in `Option`.
15pub(crate) fn is_none_or_empty_vec<T>(opt: &Option<Vec<T>>) -> bool {
16    opt.as_ref().map(|v| v.is_empty()).unwrap_or(true)
17}
18
19/// Non-streaming chat completion response type.
20pub type Chat = response::ChatGeneric<response::ChatChoice>;
21
22/// Streaming chat completion response type (SSE chunks).
23pub type ChatStream = response::ChatGeneric<response::ChatChoiceStream>;
24
25#[cfg(test)]
26mod tests {
27    use crate::{DEFAULT_BASE_URL, DeepSeekClient};
28
29    use super::request::*;
30    use super::response::*;
31    use serde_json::{Value, json};
32
33    #[test]
34    fn response_format_serializes_to_json_object() {
35        let format = ResponseFormat::json_object();
36        let value = serde_json::to_value(format).unwrap();
37        assert_eq!(value, json!({"type": "json_object"}));
38    }
39
40    #[test]
41    fn stop_supports_string_and_array() {
42        let single = Stop::from("END");
43        let many = Stop::from(vec!["END", "STOP"]);
44
45        let single_value = serde_json::to_value(single).unwrap();
46        let many_value = serde_json::to_value(many).unwrap();
47
48        assert_eq!(single_value, json!("END"));
49        assert_eq!(many_value, json!(["END", "STOP"]));
50
51        let single_back: Stop = serde_json::from_value(json!("END")).unwrap();
52        let many_back: Stop = serde_json::from_value(json!(["A", "B"])).unwrap();
53        assert!(matches!(single_back, Stop::One(_)));
54        assert!(matches!(many_back, Stop::Many(_)));
55
56        let none_back: Option<Stop> = serde_json::from_value(Value::Null).unwrap();
57        assert!(none_back.is_none());
58    }
59
60    #[test]
61    fn tool_choice_serializes_simple_and_named() {
62        let simple = ToolChoice::Simple(ChatToolChoice::Auto);
63        let simple_value = serde_json::to_value(simple).unwrap();
64        assert_eq!(simple_value, json!("auto"));
65
66        let named = ToolChoice::named(json!({"name": "get_weather"}));
67        let named_value = serde_json::to_value(named).unwrap();
68        assert_eq!(
69            named_value,
70            json!({"type": "function", "function": {"name": "get_weather"}})
71        );
72    }
73
74    #[test]
75    fn chat_message_serializes_role_and_omits_prefix_by_default() {
76        let message = ChatMessage::Assistant {
77            content: Some("Hello".to_string()),
78            name: None,
79            tool_calls: None,
80        };
81        let value = serde_json::to_value(message).unwrap();
82        assert_eq!(value.get("role"), Some(&json!("assistant")));
83        assert_eq!(value.get("content"), Some(&json!("Hello")));
84        assert!(value.get("reasoning_content").is_none());
85    }
86
87    #[test]
88    fn response_tool_call_type_serializes_as_function() {
89        let call = ToolCall::new("call_i", "get_weather", "{}");
90        let value = serde_json::to_value(call).unwrap();
91        assert_eq!(value.get("type"), Some(&json!("function")));
92    }
93
94    #[test]
95    fn builder_validation_rejects_out_of_range_values() {
96        fn base_builder() -> ChatRequestBuilder {
97            ChatRequestBuilder::default()
98                .model("deepseek-v4-pro")
99                .message(ChatMessage::User {
100                    content: "Hi".into(),
101                    name: None,
102                })
103        }
104
105        let too_hot = base_builder().temperature(2.5).build();
106        assert!(too_hot.is_err());
107
108        let bad_top_p = base_builder().top_p(1.1).build();
109        assert!(bad_top_p.is_err());
110
111        let bad_top_logprobs = base_builder().top_logprobs(21_u32).logprobs(true).build();
112        assert!(bad_top_logprobs.is_err());
113
114        let missing_logprobs = base_builder().top_logprobs(2_u32).build();
115        assert!(missing_logprobs.is_err());
116    }
117
118    #[test]
119    fn thinking_struct_serializes_type() {
120        let thinking = Thinking::disabled();
121        let value = serde_json::to_value(&thinking).unwrap();
122        assert_eq!(value.get("type"), Some(&json!("disabled")));
123
124        let req = ChatRequestBuilder::default()
125            .client(DeepSeekClient::new("sk-...", DEFAULT_BASE_URL.clone()))
126            .model("deepseek-v4-flash")
127            .message(ChatMessage::User {
128                content: "Hi".into(),
129                name: None,
130            })
131            .thinking(thinking)
132            .reasoning_effort(ReasoningEffort::Max)
133            .build();
134        // API no longer rejects reasoning_effort with disabled thinking
135        assert!(req.is_ok());
136    }
137
138    #[test]
139    fn user_content_text_serializes_as_string() {
140        let content = UserContent::Text("Hello".to_string());
141        let value = serde_json::to_value(&content).unwrap();
142        assert_eq!(value, json!("Hello"));
143    }
144
145    #[test]
146    fn user_content_parts_serializes_as_array() {
147        let content = UserContent::Parts(vec![
148            UserContentPart::text("What is in this image?"),
149            UserContentPart::file_id("file-api-abc123"),
150        ]);
151        let value = serde_json::to_value(&content).unwrap();
152        assert_eq!(
153            value,
154            json!([
155                {"type": "text", "text": "What is in this image?"},
156                {"type": "file", "file_id": "file-api-abc123"}
157            ])
158        );
159    }
160
161    #[test]
162    fn user_content_image_url_serializes() {
163        let content = UserContent::image_url("https://example.com/image.jpg");
164        let value = serde_json::to_value(&content).unwrap();
165        assert_eq!(
166            value,
167            json!([
168                {
169                    "type": "image_url",
170                    "image_url": {"url": "https://example.com/image.jpg"}
171                }
172            ])
173        );
174    }
175
176    #[test]
177    fn user_content_image_url_with_detail_serializes() {
178        let content =
179            UserContent::image_url_with_detail("https://example.com/img.png", ImageDetail::Low);
180        let value = serde_json::to_value(&content).unwrap();
181        assert_eq!(
182            value,
183            json!([
184                {
185                    "type": "image_url",
186                    "image_url": {"url": "https://example.com/img.png", "detail": "low"}
187                }
188            ])
189        );
190    }
191
192    #[test]
193    fn user_content_file_data_serializes() {
194        let content = UserContent::file_data("data:image/jpeg;base64,abc123", "photo.jpg");
195        let value = serde_json::to_value(&content).unwrap();
196        assert_eq!(
197            value,
198            json!([
199                {
200                    "type": "file",
201                    "file_data": "data:image/jpeg;base64,abc123",
202                    "filename": "photo.jpg"
203                }
204            ])
205        );
206    }
207
208    #[test]
209    fn user_content_from_str_into() {
210        let content: UserContent = "Hello".into();
211        let value = serde_json::to_value(&content).unwrap();
212        assert_eq!(value, json!("Hello"));
213    }
214
215    #[test]
216    fn user_content_from_string_into() {
217        let content: UserContent = "Hello".to_string().into();
218        let value = serde_json::to_value(&content).unwrap();
219        assert_eq!(value, json!("Hello"));
220    }
221
222    #[test]
223    fn user_content_from_vec_parts_into() {
224        let parts = vec![
225            UserContentPart::text("Hi"),
226            UserContentPart::file_id("file-xxx"),
227        ];
228        let content: UserContent = parts.into();
229        let value = serde_json::to_value(&content).unwrap();
230        assert!(value.is_array());
231    }
232
233    #[test]
234    fn chat_message_user_with_text_content() {
235        let msg = ChatMessage::User {
236            content: "Hello".into(),
237            name: None,
238        };
239        let value = serde_json::to_value(&msg).unwrap();
240        assert_eq!(value.get("role"), Some(&json!("user")));
241        assert_eq!(value.get("content"), Some(&json!("Hello")));
242    }
243
244    #[test]
245    fn chat_message_user_with_multimodal_content() {
246        let msg = ChatMessage::User {
247            content: UserContent::Parts(vec![
248                UserContentPart::text("Describe this image"),
249                UserContentPart::image_url("https://example.com/img.jpg"),
250            ]),
251            name: None,
252        };
253        let value = serde_json::to_value(&msg).unwrap();
254        assert_eq!(value.get("role"), Some(&json!("user")));
255        assert!(value.get("content").unwrap().is_array());
256        let content = value.get("content").unwrap().as_array().unwrap();
257        assert_eq!(content.len(), 2);
258        assert_eq!(content[0]["type"], json!("text"));
259        assert_eq!(content[1]["type"], json!("image_url"));
260    }
261
262    #[test]
263    fn user_content_deserializes_from_string() {
264        let content: UserContent = serde_json::from_value(json!("Hello")).unwrap();
265        assert_eq!(content, UserContent::Text("Hello".to_string()));
266    }
267
268    #[test]
269    fn user_content_part_file_with_id_deserializes() {
270        let part: UserContentPart =
271            serde_json::from_value(json!({"type": "file", "file_id": "file-api-abc123"})).unwrap();
272        assert_eq!(part, UserContentPart::file_id("file-api-abc123"));
273    }
274
275    #[test]
276    fn user_content_part_file_with_data_and_filename_deserializes() {
277        let part: UserContentPart = serde_json::from_value(json!({
278            "type": "file",
279            "file_data": "data:image/jpeg;base64,abc123",
280            "filename": "photo.jpg"
281        }))
282        .unwrap();
283        assert_eq!(
284            part,
285            UserContentPart::file_data("data:image/jpeg;base64,abc123", "photo.jpg")
286        );
287    }
288
289    #[test]
290    fn user_content_part_image_url_without_detail_deserializes() {
291        let part: UserContentPart = serde_json::from_value(json!({
292            "type": "image_url",
293            "image_url": {"url": "https://example.com/img.png"}
294        }))
295        .unwrap();
296        assert_eq!(
297            part,
298            UserContentPart::image_url("https://example.com/img.png")
299        );
300    }
301
302    #[test]
303    fn chat_message_user_multimodal_round_trips() {
304        let msg = ChatMessage::User {
305            content: UserContent::Parts(vec![
306                UserContentPart::text("What is in this image?"),
307                UserContentPart::file_id("file-api-abc123"),
308            ]),
309            name: None,
310        };
311        let value = serde_json::to_value(&msg).unwrap();
312        let back: ChatMessage = serde_json::from_value(value).unwrap();
313        assert_eq!(back, msg);
314    }
315
316    #[test]
317    fn chat_message_user_with_file_id_content() {
318        let msg = ChatMessage::User {
319            content: UserContent::file_id("file-api-abc123"),
320            name: None,
321        };
322        let value = serde_json::to_value(&msg).unwrap();
323        assert_eq!(value.get("role"), Some(&json!("user")));
324        let content = value.get("content").unwrap().as_array().unwrap();
325        assert_eq!(content[0]["type"], json!("file"));
326        assert_eq!(content[0]["file_id"], json!("file-api-abc123"));
327    }
328}