1use crate::DeepSeekClient;
6use serde::{Deserialize, Serialize};
7
8pub mod client;
9pub use client::{ChatStreamBlocking, ChatStreamItem};
10
11pub(crate) fn is_none_or_empty_vec<T>(opt: &Option<Vec<T>>) -> bool {
13 opt.as_ref().map(|v| v.is_empty()).unwrap_or(true)
14}
15
16pub type Chat = response::ChatGeneric<response::ChatChoice>;
18
19pub type ChatStream = response::ChatGeneric<response::ChatChoiceStream>;
21
22pub mod response {
23 use super::*;
24 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
26 pub struct Usage {
27 pub completion_tokens: u64,
29
30 pub prompt_tokens: u64,
32
33 pub prompt_cache_hit_tokens: u64,
35
36 pub prompt_cache_miss_tokens: u64,
38
39 pub total_tokens: u64,
41
42 pub completion_tokens_details: Option<CompletionTokensDetails>,
44 }
45 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
46 pub struct CompletionTokensDetails {
47 pub reasoning_tokens: u64,
49 }
50
51 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
53 pub struct ChatGeneric<C> {
54 pub id: String,
56
57 pub choices: Vec<C>,
58
59 pub created: u64,
61
62 pub model: String,
64 pub system_fingerprint: String,
66
67 pub object: String,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub usage: Option<Usage>,
75 }
76
77 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
79 pub struct ChatChoice {
80 pub finish_reason: FinishReason,
90
91 pub index: u64,
93
94 pub message: ChoiceMessage,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub logprobs: Option<Logprobs>,
100 }
101
102 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
104 pub struct ChatChoiceStream {
105 pub finish_reason: Option<FinishReason>,
114
115 pub index: u64,
117
118 pub delta: ChoiceMessageDelta,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub logprobs: Option<Logprobs>,
124 }
125
126 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
128 pub struct ChoiceMessage {
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub content: Option<String>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub reasoning_content: Option<String>,
136
137 #[serde(skip_serializing_if = "is_none_or_empty_vec")]
139 pub tool_calls: Option<Vec<ToolCall>>,
140
141 pub role: Role,
143 }
144
145 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
147 pub struct ChoiceMessageDelta {
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub content: Option<String>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub reasoning_content: Option<String>,
155 #[serde(skip_serializing_if = "is_none_or_empty_vec")]
156 pub tool_calls: Option<Vec<ToolCall>>,
157 #[serde(skip_serializing_if = "Option::is_none")]
161 pub role: Option<Role>,
162 }
163
164 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
166 #[serde(rename_all = "snake_case")]
167 pub enum Role {
168 System,
169 User,
170 Assistant,
171 Tool,
172 }
173
174 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
176 pub struct ToolCall {
177 pub id: String,
179 #[serde(rename = "type")]
180
181 pub typ: ToolCallType,
185
186 pub function: ToolCallFunction,
188 }
189
190 impl ToolCall {
191 pub fn new(
193 id: impl Into<String>,
194 name: impl Into<String>,
195 arguments: impl Into<String>,
196 ) -> Self {
197 ToolCall {
198 id: id.into(),
199 typ: ToolCallType::Function,
200 function: ToolCallFunction {
201 name: name.into(),
202 arguments: arguments.into(),
203 },
204 }
205 }
206 }
207
208 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
210 #[serde(rename_all = "snake_case")]
211 pub enum ToolCallType {
212 Function,
213 }
214
215 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
217 pub struct ToolCallFunction {
218 pub name: String,
220 pub arguments: String,
225 }
226 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
228 #[serde(rename_all = "snake_case")]
229 pub enum FinishReason {
230 Stop,
231 Length,
232 ContentFilter,
233 ToolCalls,
234 InsufficientSystemResources,
235 }
236 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
238 pub struct Logprobs {
239 #[serde(skip_serializing_if = "is_none_or_empty_vec")]
240 pub content: Option<Vec<LogprobsContent>>,
241 #[serde(skip_serializing_if = "is_none_or_empty_vec")]
242 pub reasoning_content: Option<Vec<LogprobsReasoningContent>>,
243 }
244 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
246 pub struct LogprobsContent {
247 pub token: String,
248 pub logprob: f64,
249 pub bytes: Option<Vec<u8>>,
250 pub top_logprobs: Vec<TopLogprobs>,
251 }
252
253 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
255 pub struct TopLogprobs {
256 pub token: String,
257 pub logprob: f64,
258 pub bytes: Option<Vec<u8>>,
259 }
260 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
262 pub struct LogprobsReasoningContent {
263 pub token: String,
264 pub logprob: f64,
265 pub bytes: Option<Vec<u8>>,
266 pub top_logprobs: Vec<TopLogprobs>,
267 }
268}
269
270pub mod request {
272 use super::*;
273 use derive_builder::Builder;
274 pub(crate) fn is_none_or_empty_stop(opt: &Option<Stop>) -> bool {
275 opt.as_ref().map(|stop| stop.is_empty()).unwrap_or(true)
276 }
277
278 #[derive(Clone, Debug, PartialEq, Serialize, Builder)]
280 #[builder(
281 pattern = "owned",
282 setter(into, strip_option),
283 build_fn(validate = "Self::validate"),
284 name = "ChatRequestBuilder"
285 )]
286 pub struct ChatRequest {
287 #[serde(skip_serializing)]
288 pub client: DeepSeekClient,
289
290 #[builder(setter(each(name = "message", into)))]
292 pub messages: Vec<ChatMessage>,
293
294 pub model: String,
298
299 #[builder(default)]
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub thinking: Option<Thinking>,
303
304 #[builder(default)]
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub reasoning_effort: Option<ReasoningEffort>,
315
316 #[builder(default)]
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub max_tokens: Option<u32>,
324
325 #[builder(default)]
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub response_format: Option<ResponseFormat>,
334
335 #[builder(default)]
337 #[serde(skip_serializing_if = "is_none_or_empty_stop")]
338 pub stop: Option<Stop>,
339
340 #[builder(default)]
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub stream: Option<bool>,
346
347 #[builder(default)]
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub stream_options: Option<StreamOptions>,
351
352 #[builder(default)]
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub temperature: Option<f64>,
361
362 #[builder(default)]
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub top_p: Option<f64>,
374
375 #[builder(default, setter(each(name = "tool", into)))]
379 #[serde(skip_serializing_if = "Vec::is_empty")]
380 pub tools: Vec<Tool>,
381
382 #[builder(default)]
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub tool_choice: Option<ToolChoice>,
391
392 #[builder(default)]
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub logprobs: Option<bool>,
397
398 #[builder(default)]
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub top_logprobs: Option<u32>,
405
406 #[builder(default)]
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub user_id: Option<String>,
416 }
417 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
419 #[serde(tag = "role", rename_all = "snake_case")]
420 pub enum ChatMessage {
421 System {
422 content: String,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 name: Option<String>,
427 },
428 User {
429 content: String,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 name: Option<String>,
434 },
435 Assistant {
436 #[serde(skip_serializing_if = "Option::is_none")]
438 content: Option<String>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 name: Option<String>,
442
443 #[serde(skip_serializing_if = "super::is_none_or_empty_vec")]
444 tool_calls: Option<Vec<super::response::ToolCall>>,
445 },
446 Tool {
447 content: String,
449 tool_call_id: String,
451 },
452 }
453 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
455 #[serde(rename_all = "snake_case")]
456 pub enum ReasoningEffort {
457 High,
458 Max,
459 }
460 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
462 pub struct ResponseFormat {
463 #[serde(rename = "type")]
466 pub(crate) typ: ResponseFormatType,
467 }
468 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
470 #[serde(rename_all = "snake_case")]
471 pub(crate) enum ResponseFormatType {
472 Text,
473 JsonObject,
474 }
475
476 impl ResponseFormat {
477 pub fn text() -> Self {
478 ResponseFormat {
479 typ: ResponseFormatType::Text,
480 }
481 }
482
483 pub fn json_object() -> Self {
484 ResponseFormat {
485 typ: ResponseFormatType::JsonObject,
486 }
487 }
488 }
489
490 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
492 #[serde(untagged)]
493 pub enum Stop {
494 One(String),
495 Many(Vec<String>),
496 }
497
498 impl Stop {
499 fn is_empty(&self) -> bool {
500 match self {
501 Stop::One(value) => value.is_empty(),
502 Stop::Many(values) => values.is_empty(),
503 }
504 }
505 }
506
507 impl From<String> for Stop {
508 fn from(value: String) -> Self {
509 Stop::One(value)
510 }
511 }
512
513 impl From<&str> for Stop {
514 fn from(value: &str) -> Self {
515 Stop::One(value.to_string())
516 }
517 }
518
519 impl<T> From<Vec<T>> for Stop
520 where
521 T: Into<String>,
522 {
523 fn from(values: Vec<T>) -> Self {
524 Stop::Many(values.into_iter().map(Into::into).collect())
525 }
526 }
527 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
529 pub struct StreamOptions {
530 pub include_usage: bool,
535 }
536 #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
538 pub struct Tool {
539 #[serde(rename = "type")]
541 pub typ: ToolType,
542 pub function: ToolFunctionDefinition,
543 }
544
545 impl Tool {
546 pub fn new(
547 name: impl Into<String>,
548 description: impl Into<String>,
549 parameters: Option<serde_json::Value>,
550 ) -> Self {
551 Tool {
552 typ: ToolType::Function,
553 function: ToolFunctionDefinition {
554 name: name.into(),
555 description: description.into(),
556 parameters,
557 },
558 }
559 }
560 }
561
562 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
564 #[serde(rename_all = "snake_case")]
565 pub enum ToolType {
566 Function,
567 }
568
569 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
571 pub struct ToolFunctionDefinition {
572 pub description: String,
575 pub name: String,
578 #[serde(skip_serializing_if = "Option::is_none")]
584 pub parameters: Option<serde_json::Value>,
585 }
586 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
588 #[serde(untagged)]
589 pub enum ToolChoice {
590 Simple(ChatToolChoice),
592 Named(ChatNamedToolChoice),
594 }
595
596 impl ToolChoice {
597 pub fn named(function: serde_json::Value) -> Self {
598 ToolChoice::Named(ChatNamedToolChoice {
599 typ: ToolType::Function,
600 function,
601 })
602 }
603
604 pub fn none() -> Self {
605 ToolChoice::Simple(ChatToolChoice::None)
606 }
607
608 pub fn auto() -> Self {
609 ToolChoice::Simple(ChatToolChoice::Auto)
610 }
611
612 pub fn required() -> Self {
613 ToolChoice::Simple(ChatToolChoice::Required)
614 }
615 }
616
617 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
619 #[serde(rename_all = "snake_case")]
620 pub enum ChatToolChoice {
621 None,
622 Auto,
623 Required,
624 }
625 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
627 pub struct ChatNamedToolChoice {
628 #[serde(rename = "type")]
632 pub typ: ToolType,
633
634 pub function: serde_json::Value,
635 }
636
637 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
638 pub struct Thinking {
639 #[serde(rename = "type")]
645 pub(crate) typ: ThinkingType,
646 }
647
648 impl Thinking {
649 pub fn enabled() -> Self {
650 Thinking {
651 typ: ThinkingType::Enabled,
652 }
653 }
654
655 pub fn disabled() -> Self {
656 Thinking {
657 typ: ThinkingType::Disabled,
658 }
659 }
660 }
661
662 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
663 #[serde(rename_all = "snake_case")]
664 pub(crate) enum ThinkingType {
665 Enabled,
666 Disabled,
667 }
668
669 impl ChatRequestBuilder {
670 fn validate(&self) -> Result<(), String> {
671 if let Some(temperature) = self.temperature.flatten()
674 && !(0.0..=2.0).contains(&temperature)
675 {
676 return Err("temperature must be between 0 and 2".to_string());
677 }
678
679 if let Some(top_p) = self.top_p.flatten()
680 && !(0.0..=1.0).contains(&top_p)
681 {
682 return Err("top_p must be between 0 and 1".to_string());
683 }
684
685 if let Some(top_logprobs) = self.top_logprobs.flatten() {
686 if top_logprobs > 20 {
687 return Err("top_logprobs must be <= 20".to_string());
688 }
689 if self.logprobs.flatten() != Some(true) {
690 return Err("top_logprobs requires logprobs=true".to_string());
691 }
692 }
693
694 if let Some(stream) = self.stream.flatten()
695 && !stream
696 && self.stream_options.is_some()
697 {
698 return Err("stream_options cannot be set when stream is false".to_string());
699 }
700
701 if let Some(stop) = self.stop.as_ref().and_then(|s| s.as_ref())
702 && let Stop::Many(values) = stop
703 && values.len() > 16
704 {
705 return Err("a maximum of 16 stop sequences are allowed".to_string());
706 }
707
708 if let Some(user_id) = self.user_id.as_ref().and_then(|u| u.as_ref()) {
709 if user_id.len() > 512 {
710 return Err("user_id must be at most 512 characters".to_string());
711 }
712 if !user_id
713 .chars()
714 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
715 {
716 return Err("user_id must only contain [a-zA-Z0-9\\-_]".to_string());
717 }
718 }
719
720 Ok(())
721 }
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use crate::{DEFAULT_BASE_URL, DeepSeekClient};
728
729 use super::request::*;
730 use super::response::*;
731 use serde_json::{Value, json};
732
733 #[test]
734 fn response_format_serializes_to_json_object() {
735 let format = ResponseFormat::json_object();
736 let value = serde_json::to_value(format).unwrap();
737 assert_eq!(value, json!({"type": "json_object"}));
738 }
739
740 #[test]
741 fn stop_supports_string_and_array() {
742 let single = Stop::from("END");
743 let many = Stop::from(vec!["END", "STOP"]);
744
745 let single_value = serde_json::to_value(single).unwrap();
746 let many_value = serde_json::to_value(many).unwrap();
747
748 assert_eq!(single_value, json!("END"));
749 assert_eq!(many_value, json!(["END", "STOP"]));
750
751 let single_back: Stop = serde_json::from_value(json!("END")).unwrap();
752 let many_back: Stop = serde_json::from_value(json!(["A", "B"])).unwrap();
753 assert!(matches!(single_back, Stop::One(_)));
754 assert!(matches!(many_back, Stop::Many(_)));
755
756 let none_back: Option<Stop> = serde_json::from_value(Value::Null).unwrap();
757 assert!(none_back.is_none());
758 }
759
760 #[test]
761 fn tool_choice_serializes_simple_and_named() {
762 let simple = ToolChoice::Simple(ChatToolChoice::Auto);
763 let simple_value = serde_json::to_value(simple).unwrap();
764 assert_eq!(simple_value, json!("auto"));
765
766 let named = ToolChoice::named(json!({"name": "get_weather"}));
767 let named_value = serde_json::to_value(named).unwrap();
768 assert_eq!(
769 named_value,
770 json!({"type": "function", "function": {"name": "get_weather"}})
771 );
772 }
773
774 #[test]
775 fn chat_message_serializes_role_and_omits_prefix_by_default() {
776 let message = ChatMessage::Assistant {
777 content: Some("Hello".to_string()),
778 name: None,
779 tool_calls: None,
780 };
781 let value = serde_json::to_value(message).unwrap();
782 assert_eq!(value.get("role"), Some(&json!("assistant")));
783 assert_eq!(value.get("content"), Some(&json!("Hello")));
784 assert!(value.get("reasoning_content").is_none());
785 }
786
787 #[test]
788 fn response_tool_call_type_serializes_as_function() {
789 let call = ToolCall::new("call_i", "get_weather", "{}");
790 let value = serde_json::to_value(call).unwrap();
791 assert_eq!(value.get("type"), Some(&json!("function")));
792 }
793
794 #[test]
795 fn builder_validation_rejects_out_of_range_values() {
796 fn base_builder() -> ChatRequestBuilder {
797 ChatRequestBuilder::default()
798 .model("deepseek-v4-pro")
799 .message(ChatMessage::User {
800 content: "Hi".to_string(),
801 name: None,
802 })
803 }
804
805 let too_hot = base_builder().temperature(2.5).build();
806 assert!(too_hot.is_err());
807
808 let bad_top_p = base_builder().top_p(1.1).build();
809 assert!(bad_top_p.is_err());
810
811 let bad_top_logprobs = base_builder().top_logprobs(21_u32).logprobs(true).build();
812 assert!(bad_top_logprobs.is_err());
813
814 let missing_logprobs = base_builder().top_logprobs(2_u32).build();
815 assert!(missing_logprobs.is_err());
816 }
817
818 #[test]
819 fn thinking_struct_serializes_type() {
820 let thinking = Thinking::disabled();
821 let value = serde_json::to_value(&thinking).unwrap();
822 assert_eq!(value.get("type"), Some(&json!("disabled")));
823
824 let req = ChatRequestBuilder::default()
825 .client(DeepSeekClient::new("sk-...", DEFAULT_BASE_URL.clone()))
826 .model("deepseek-v4-flash")
827 .message(ChatMessage::User {
828 content: "Hi".to_string(),
829 name: None,
830 })
831 .thinking(thinking)
832 .reasoning_effort(ReasoningEffort::Max)
833 .build();
834 assert!(req.is_ok());
836 }
837}