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 client implementation.
5use crate::DeepSeekClient;
6use serde::{Deserialize, Serialize};
7
8pub mod client;
9pub use client::{ChatStreamBlocking, ChatStreamItem};
10
11/// Helper to skip serialization of empty `Vec` fields wrapped in `Option`.
12pub(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
16/// Non-streaming chat completion response type.
17pub type Chat = response::ChatGeneric<response::ChatChoice>;
18
19/// Streaming chat completion response type (SSE chunks).
20pub type ChatStream = response::ChatGeneric<response::ChatChoiceStream>;
21
22pub mod response {
23    use super::*;
24    /// Token usage statistics for a request.
25    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
26    pub struct Usage {
27        /// Number of tokens in the generated completion.
28        pub completion_tokens: u64,
29
30        /// Number of tokens in the prompt. It equals prompt_cache_hit_tokens + prompt_cache_miss_tokens.
31        pub prompt_tokens: u64,
32
33        /// Number of tokens in the prompt that hits the context cache.
34        pub prompt_cache_hit_tokens: u64,
35
36        /// Number of tokens in the prompt that misses the context cache.
37        pub prompt_cache_miss_tokens: u64,
38
39        /// Total number of tokens used in the request (prompt + completion).
40        pub total_tokens: u64,
41
42        /// Breakdown of tokens used in a completion.
43        pub completion_tokens_details: Option<CompletionTokensDetails>,
44    }
45    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
46    pub struct CompletionTokensDetails {
47        /// Tokens generated by the model for reasoning.
48        pub reasoning_tokens: u64,
49    }
50
51    /// Generic chat response container.
52    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
53    pub struct ChatGeneric<C> {
54        /// A unique identifier for the chat completion.
55        pub id: String,
56
57        pub choices: Vec<C>,
58
59        /// The Unix timestamp (in seconds) of when the chat completion was created.
60        pub created: u64,
61
62        /// The model used for the chat completion.
63        pub model: String,
64        /// This fingerprint represents the backend configuration that the model runs with.
65        pub system_fingerprint: String,
66
67        /// Possible values: [`chat.completion`]
68        ///
69        /// The object type, which is always `chat.completion`.
70        pub object: String,
71
72        /// Usage statistics for the completion request.
73        #[serde(skip_serializing_if = "Option::is_none")]
74        pub usage: Option<Usage>,
75    }
76
77    /// Non-streaming choice result.
78    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
79    pub struct ChatChoice {
80        /// Possible values: [`stop`, `length`, `content_filter`, `tool_calls`,
81        /// `insufficient_system_resource`]
82        ///
83        /// The reason the model stopped generating tokens.
84        /// This will be `stop` if the model hit a natural stop point or a provided stop sequence,
85        /// `length` if the maximum number of tokens specified in the request was reached,
86        /// `content_filter` if content was omitted due to a flag from our content filters,
87        /// `tool_calls` if the model called a tool,
88        /// or `insufficient_system_resource` if the request is interrupted due to insufficient resource of the inference system.
89        pub finish_reason: FinishReason,
90
91        /// The index of the choice in the list of choices.
92        pub index: u64,
93
94        /// A chat completion message generated by the model.
95        pub message: ChoiceMessage,
96
97        /// Log probability information for the choice.
98        #[serde(skip_serializing_if = "Option::is_none")]
99        pub logprobs: Option<Logprobs>,
100    }
101
102    /// Streaming choice delta.
103    #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
104    pub struct ChatChoiceStream {
105        /// Possible values: [`stop`, `length`, `content_filter`, `tool_calls`, `insufficient_system_resource`]
106        ///
107        /// The reason the model stopped generating tokens.
108        /// This will be `stop` if the model hit a natural stop point or a provided stop sequence,
109        /// `length` if the maximum number of tokens specified in the request was reached,
110        /// `content_filter` if content was omitted due to a flag from our content filters,
111        /// `tool_calls` if the model called a tool,
112        /// or `insufficient_system_resource` if the request is interrupted due to insufficient resource of the inference system.
113        pub finish_reason: Option<FinishReason>,
114
115        /// The index of the choice in the list of choices.
116        pub index: u64,
117
118        /// A chat completion delta generated by streamed model responses.
119        pub delta: ChoiceMessageDelta,
120
121        /// Log probability information for the choice.
122        #[serde(skip_serializing_if = "Option::is_none")]
123        pub logprobs: Option<Logprobs>,
124    }
125
126    /// Assistant message content in non-streaming responses.
127    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
128    pub struct ChoiceMessage {
129        /// The contents of the message.
130        #[serde(skip_serializing_if = "Option::is_none")]
131        pub content: Option<String>,
132
133        /// For thinking mode only. The reasoning contents of the assistant message, before the final answer.
134        #[serde(skip_serializing_if = "Option::is_none")]
135        pub reasoning_content: Option<String>,
136
137        /// The tool calls generated by the model.
138        #[serde(skip_serializing_if = "is_none_or_empty_vec")]
139        pub tool_calls: Option<Vec<ToolCall>>,
140
141        /// The role of the author of this message.
142        pub role: Role,
143    }
144
145    /// Assistant message delta in streaming responses.
146    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
147    pub struct ChoiceMessageDelta {
148        /// The contents of the chunk message.
149        #[serde(skip_serializing_if = "Option::is_none")]
150        pub content: Option<String>,
151
152        /// For thinking mode only. The reasoning contents of the assistant message, before the final answer.
153        #[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        /// Possible values: [assistant]
158        ///
159        /// The role of the author of this message.
160        #[serde(skip_serializing_if = "Option::is_none")]
161        pub role: Option<Role>,
162    }
163
164    /// Role of a chat message.
165    #[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    /// Tool call emitted by the model.
175    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
176    pub struct ToolCall {
177        /// The ID of the tool call.
178        pub id: String,
179        #[serde(rename = "type")]
180
181        /// Possible values: [`function`]
182        ///
183        ///The type of the tool. Currently, only `function` is supported.
184        pub typ: ToolCallType,
185
186        /// The function that the model called.
187        pub function: ToolCallFunction,
188    }
189
190    impl ToolCall {
191        /// Build a function tool call with an id, name, and arguments JSON string.
192        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    /// Tool call type.
209    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
210    #[serde(rename_all = "snake_case")]
211    pub enum ToolCallType {
212        Function,
213    }
214
215    /// Tool call function payload.
216    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
217    pub struct ToolCallFunction {
218        /// The name of the function to call.
219        pub name: String,
220        /// The arguments to call the function with, as generated by the model in JSON format.
221        /// Note that the model does not always generate valid JSON,
222        /// and may hallucinate parameters not defined by your function schema.
223        /// Validate the arguments in your code before calling your function.
224        pub arguments: String,
225    }
226    /// Reason for completion termination.
227    #[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    /// Token-level log probability data.
237    #[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    /// Logprobs for content tokens.
245    #[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    /// Top logprob candidates for a token.
254    #[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    /// Logprobs for reasoning tokens.
261    #[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
270/// Request payloads for `/chat/completions`.
271pub 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    /// Chat completion request body.
279    #[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        /// A list of messages comprising the conversation so far.
291        #[builder(setter(each(name = "message", into)))]
292        pub messages: Vec<ChatMessage>,
293
294        /// Possible values: [`deepseek-v4-flash`, `deepseek-v4-pro`]
295        ///
296        /// ID of the model to use.
297        pub model: String,
298
299        /// Controls the switch between thinking and non-thinking mode.
300        #[builder(default)]
301        #[serde(skip_serializing_if = "Option::is_none")]
302        pub thinking: Option<Thinking>,
303
304        /// Possible values: [`high`, `max`]
305        ///
306        /// Controls the reasoning effort of the model.
307        /// The default effort is `high` for regular requests;
308        /// for some complex agent requests (such as Claude Code, OpenCode),
309        /// effort is automatically set to `max`.
310        /// For compatibility, `low` and `medium` are mapped to `high`,
311        /// and `xhigh` is mapped to `max`.
312        #[builder(default)]
313        #[serde(skip_serializing_if = "Option::is_none")]
314        pub reasoning_effort: Option<ReasoningEffort>,
315
316        /// The maximum number of tokens that can be generated in the chat completion.
317        ///
318        /// The total length of input tokens and generated tokens is limited by the model's context length.
319        ///
320        /// For the value range and default value, please refer to the [documentation](https://api-docs.deepseek.com/quick_start/pricing).
321        #[builder(default)]
322        #[serde(skip_serializing_if = "Option::is_none")]
323        pub max_tokens: Option<u32>,
324
325        /// An object specifying the format that the model must output.
326        /// Setting to { "type": "json_object" } enables JSON Output,
327        /// which guarantees the message the model generates is valid JSON.
328        ///
329        /// **Important**: When using JSON Output, you must also instruct the model to produce JSON yourself via a system or user message.
330        /// Without this, the model may generate an unending stream of whitespace until the generation reaches the token limit, resulting in a long-running and seemingly "stuck" request. Also note that the message content may be partially cut off if finish_reason="length", which indicates the generation exceeded max_tokens or the conversation exceeded the max context length.
331        #[builder(default)]
332        #[serde(skip_serializing_if = "Option::is_none")]
333        pub response_format: Option<ResponseFormat>,
334
335        /// Up to 16 sequences where the API will stop generating further tokens.
336        #[builder(default)]
337        #[serde(skip_serializing_if = "is_none_or_empty_stop")]
338        pub stop: Option<Stop>,
339
340        /// If set, partial message deltas will be sent.
341        /// Tokens will be sent as data-only server-sent events (SSE) as they become available,
342        /// with the stream terminated by a `data: [DONE]`` message.
343        #[builder(default)]
344        #[serde(skip_serializing_if = "Option::is_none")]
345        pub stream: Option<bool>,
346
347        /// Options for streaming response. Only set this when you set `stream: true`.
348        #[builder(default)]
349        #[serde(skip_serializing_if = "Option::is_none")]
350        pub stream_options: Option<StreamOptions>,
351
352        /// Possible values: `<= 2`
353        ///
354        /// Default value: `1`
355        ///
356        /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
357        /// We generally recommend altering this or `top_p` but not both.
358        #[builder(default)]
359        #[serde(skip_serializing_if = "Option::is_none")]
360        pub temperature: Option<f64>,
361
362        /// Possible values: `<= 1`
363        ///
364        /// Default value: `1`
365        ///
366        /// An alternative to sampling with temperature, called nucleus sampling,
367        /// where the model considers the results of the tokens with top_p probability mass.
368        /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
369        ///
370        /// We generally recommend altering this or `temperature` but not both.
371        #[builder(default)]
372        #[serde(skip_serializing_if = "Option::is_none")]
373        pub top_p: Option<f64>,
374
375        /// A list of tools the model may call. Currently, only functions are supported as a tool.
376        /// Use this to provide a list of functions the model may generate JSON inputs for.
377        /// A max of 128 functions are supported.
378        #[builder(default, setter(each(name = "tool", into)))]
379        #[serde(skip_serializing_if = "Vec::is_empty")]
380        pub tools: Vec<Tool>,
381
382        /// Controls which (if any) tool is called by the model.
383        /// `none` means the model will not call any tool and instead generates a message.
384        /// `auto` means the model can pick between generating a message or calling one or more tools.
385        /// `required` means the model must call one or more tools.
386        /// Specifying a particular tool via `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool.
387        /// `none` is the default when no tools are present. `auto` is the default if tools are present.
388        #[builder(default)]
389        #[serde(skip_serializing_if = "Option::is_none")]
390        pub tool_choice: Option<ToolChoice>,
391
392        /// Whether to return log probabilities of the output tokens or not.
393        /// If true, returns the log probabilities of each output token returned in the `content` of `message`.
394        #[builder(default)]
395        #[serde(skip_serializing_if = "Option::is_none")]
396        pub logprobs: Option<bool>,
397
398        /// Possible values: `<= 20`
399        ///
400        /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position,
401        /// each with an associated log probability. `logprobs` must be set to `true` if this parameter is used.
402        #[builder(default)]
403        #[serde(skip_serializing_if = "Option::is_none")]
404        pub top_logprobs: Option<u32>,
405
406        /// A custom `user_id`. Allowed character set is `[a-zA-Z0-9\-_]`, with a maximum length of 512.
407        /// Do not include user privacy information in the `user_id`.
408
409        /// `user_id` can be used to distinguish user identities on your side to help us with content safety review.
410        /// `user_id` can be used for KVCache isolation for privacy management.
411        /// `user_id` can be used for scheduling isolation of users on your business side.
412        /// For more details on the `user_id` parameter, please refer to [Rate Limit & Isolation](https://api-docs.deepseek.com/quick_start/rate_limit)
413        #[builder(default)]
414        #[serde(skip_serializing_if = "Option::is_none")]
415        pub user_id: Option<String>,
416    }
417    /// Chat message variants.
418    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
419    #[serde(tag = "role", rename_all = "snake_case")]
420    pub enum ChatMessage {
421        System {
422            /// The contents of the system message.
423            content: String,
424            /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
425            #[serde(skip_serializing_if = "Option::is_none")]
426            name: Option<String>,
427        },
428        User {
429            /// The contents of the user message.
430            content: String,
431            /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
432            #[serde(skip_serializing_if = "Option::is_none")]
433            name: Option<String>,
434        },
435        Assistant {
436            /// The contents of the assistant message.
437            #[serde(skip_serializing_if = "Option::is_none")]
438            content: Option<String>,
439            /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
440            #[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            /// The contents of the tool message.
448            content: String,
449            /// Tool call that this message is responding to.
450            tool_call_id: String,
451        },
452    }
453    /// Reasoning effort hints for the model.
454    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
455    #[serde(rename_all = "snake_case")]
456    pub enum ReasoningEffort {
457        High,
458        Max,
459    }
460    /// Response format configuration.
461    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
462    pub struct ResponseFormat {
463        /// Default value: `text`
464        /// Must be one of `text` or `json_object`.
465        #[serde(rename = "type")]
466        pub(crate) typ: ResponseFormatType,
467    }
468    /// Supported response format types.
469    #[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    /// Stop sequences for generation.
491    #[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    /// Streaming options for SSE responses.
528    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
529    pub struct StreamOptions {
530        /// If set, an additional chunk will be streamed before the `data: [DONE]` message.
531        /// The `usage` field on this chunk shows the token usage statistics for the entire request,
532        /// and the `choices` field will always be an empty array.
533        /// All other chunks will also include a `usage` field, but with a null value.
534        pub include_usage: bool,
535    }
536    /// Tool definition used by the model.
537    #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
538    pub struct Tool {
539        /// The type of the tool. Currently, only `function` is supported.
540        #[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    /// Tool type.
563    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
564    #[serde(rename_all = "snake_case")]
565    pub enum ToolType {
566        Function,
567    }
568
569    /// Tool function definition.
570    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
571    pub struct ToolFunctionDefinition {
572        /// A description of what the function does,
573        /// used by the model to choose when and how to call the function.
574        pub description: String,
575        /// The name of the function to be called. Must be a-z, A-Z, 0-9,
576        /// or contain underscores and dashes, with a maximum length of 64.
577        pub name: String,
578        /// The parameters the functions accepts, described as a JSON Schema object.
579        /// See the [Tool Calls Guide](https://api-docs.deepseek.com/guides/tool_calls) for examples,
580        /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
581        ///
582        /// Omitting `parameters` defines a function with an empty parameter list.
583        #[serde(skip_serializing_if = "Option::is_none")]
584        pub parameters: Option<serde_json::Value>,
585    }
586    /// Tool choice configuration.
587    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
588    #[serde(untagged)]
589    pub enum ToolChoice {
590        /// Possible values: [`none`, `auto`, `required`]
591        Simple(ChatToolChoice),
592        /// {"type":"function","function":{...}}
593        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    /// Tool choice values.
618    #[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    /// Named tool choice configuration.
626    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
627    pub struct ChatNamedToolChoice {
628        /// Possible values: [`function`]
629        ///
630        /// The type of the tool. Currently, only `function` is supported.
631        #[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        /// Possible values: [`enabled`, `disabled`]
640        ///
641        /// Default value: `enabled`
642        ///
643        /// If set to `enabled`, then use thinking mode. If set to `disabled`, then use non-thinking model.
644        #[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            // derive_builder + strip_option makes Option<T> fields become Option<Option<T>> here;
672            // flatten() treats "unset" and "explicit None" uniformly for validation.
673            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        // API no longer rejects reasoning_effort with disabled thinking
835        assert!(req.is_ok());
836    }
837}