Skip to main content

deepseek_sdk/chat/
request.rs

1use super::*;
2use derive_builder::Builder;
3
4pub(crate) fn is_none_or_empty_stop(opt: &Option<Stop>) -> bool {
5    opt.as_ref().map(|stop| stop.is_empty()).unwrap_or(true)
6}
7
8/// Chat completion request body.
9#[derive(Clone, Debug, PartialEq, Serialize, Builder)]
10#[builder(
11    pattern = "owned",
12    setter(into, strip_option),
13    build_fn(validate = "Self::validate"),
14    name = "ChatRequestBuilder"
15)]
16pub struct ChatRequest {
17    #[serde(skip_serializing)]
18    pub client: DeepSeekClient,
19
20    /// A list of messages comprising the conversation so far.
21    #[builder(setter(each(name = "message", into)))]
22    pub messages: Vec<ChatMessage>,
23
24    /// Possible values: \`deepseek-v4-flash\`, \`deepseek-v4-pro\`
25    ///
26    /// ID of the model to use.
27    pub model: String,
28
29    /// Controls the switch between thinking and non-thinking mode.
30    #[builder(default)]
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub thinking: Option<Thinking>,
33
34    /// Possible values: [`high`, `max`]
35    ///
36    /// Controls the reasoning effort of the model.
37    /// The default effort is `high` for regular requests;
38    /// for some complex agent requests (such as Claude Code, OpenCode),
39    /// effort is automatically set to `max`.
40    /// For compatibility, `low` and `medium` are mapped to `high`,
41    /// and `xhigh` is mapped to `max`.
42    #[builder(default)]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub reasoning_effort: Option<ReasoningEffort>,
45
46    /// The maximum number of tokens that can be generated in the chat completion.
47    ///
48    /// The total length of input tokens and generated tokens is limited by the model's context length.
49    ///
50    /// For the value range and default value, please refer to the [documentation](https://api-docs.deepseek.com/quick_start/pricing).
51    #[builder(default)]
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub max_tokens: Option<u32>,
54
55    /// An object specifying the format that the model must output.
56    /// Setting to { "type": "json_object" } enables JSON Output,
57    /// which guarantees the message the model generates is valid JSON.
58    ///
59    /// **Important**: When using JSON Output, you must also instruct the model to produce JSON yourself via a system or user message.
60    /// 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.
61    #[builder(default)]
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub response_format: Option<ResponseFormat>,
64
65    /// Up to 16 sequences where the API will stop generating further tokens.
66    #[builder(default)]
67    #[serde(skip_serializing_if = "is_none_or_empty_stop")]
68    pub stop: Option<Stop>,
69
70    /// If set, partial message deltas will be sent.
71    /// Tokens will be sent as data-only server-sent events (SSE) as they become available,
72    /// with the stream terminated by a \`data: \[DONE\]\` message.
73    #[builder(default)]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub stream: Option<bool>,
76
77    /// Options for streaming response. Only set this when you set `stream: true`.
78    #[builder(default)]
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub stream_options: Option<StreamOptions>,
81
82    /// Possible values: `<= 2`
83    ///
84    /// Default value: `1`
85    ///
86    /// 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.
87    /// We generally recommend altering this or `top_p` but not both.
88    #[builder(default)]
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub temperature: Option<f64>,
91
92    /// Possible values: `<= 1`
93    ///
94    /// Default value: `1`
95    ///
96    /// An alternative to sampling with temperature, called nucleus sampling,
97    /// where the model considers the results of the tokens with top_p probability mass.
98    /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
99    ///
100    /// We generally recommend altering this or `temperature` but not both.
101    #[builder(default)]
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub top_p: Option<f64>,
104
105    /// A list of tools the model may call. Currently, only functions are supported as a tool.
106    /// Use this to provide a list of functions the model may generate JSON inputs for.
107    /// A max of 128 functions are supported.
108    #[builder(default, setter(each(name = "tool", into)))]
109    #[serde(skip_serializing_if = "Vec::is_empty")]
110    pub tools: Vec<Tool>,
111
112    /// Controls which (if any) tool is called by the model.
113    /// `none` means the model will not call any tool and instead generates a message.
114    /// `auto` means the model can pick between generating a message or calling one or more tools.
115    /// `required` means the model must call one or more tools.
116    /// Specifying a particular tool via `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool.
117    /// `none` is the default when no tools are present. `auto` is the default if tools are present.
118    #[builder(default)]
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub tool_choice: Option<ToolChoice>,
121
122    /// Whether to return log probabilities of the output tokens or not.
123    /// If true, returns the log probabilities of each output token returned in the `content` of `message`.
124    #[builder(default)]
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub logprobs: Option<bool>,
127
128    /// Possible values: `<= 20`
129    ///
130    /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position,
131    /// each with an associated log probability. `logprobs` must be set to `true` if this parameter is used.
132    #[builder(default)]
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub top_logprobs: Option<u32>,
135
136    /// A custom `user_id`. Allowed character set is `[a-zA-Z0-9\-_]`, with a maximum length of 512.
137    /// Do not include user privacy information in the `user_id`.
138
139    /// `user_id` can be used to distinguish user identities on your side to help us with content safety review.
140    /// `user_id` can be used for KVCache isolation for privacy management.
141    /// `user_id` can be used for scheduling isolation of users on your business side.
142    /// For more details on the `user_id` parameter, please refer to [Rate Limit & Isolation](https://api-docs.deepseek.com/quick_start/rate_limit)
143    #[builder(default)]
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub user_id: Option<String>,
146}
147/// Chat message variants.
148#[non_exhaustive]
149#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
150#[serde(tag = "role", rename_all = "snake_case")]
151pub enum ChatMessage {
152    System {
153        /// The contents of the system message.
154        content: String,
155        /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
156        #[serde(skip_serializing_if = "Option::is_none")]
157        name: Option<String>,
158    },
159    User {
160        /// The contents of the user message.
161        content: String,
162        /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
163        #[serde(skip_serializing_if = "Option::is_none")]
164        name: Option<String>,
165    },
166    Assistant {
167        /// The contents of the assistant message.
168        #[serde(skip_serializing_if = "Option::is_none")]
169        content: Option<String>,
170        /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
171        #[serde(skip_serializing_if = "Option::is_none")]
172        name: Option<String>,
173
174        #[serde(skip_serializing_if = "super::is_none_or_empty_vec")]
175        tool_calls: Option<Vec<super::response::ToolCall>>,
176    },
177    Tool {
178        /// The contents of the tool message.
179        content: String,
180        /// Tool call that this message is responding to.
181        tool_call_id: String,
182    },
183}
184/// Reasoning effort hints for the model.
185#[non_exhaustive]
186#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
187#[serde(rename_all = "snake_case")]
188pub enum ReasoningEffort {
189    High,
190    Max,
191}
192/// Response format configuration.
193#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
194pub struct ResponseFormat {
195    /// Default value: `text`
196    /// Must be one of `text` or `json_object`.
197    #[serde(rename = "type")]
198    pub(crate) typ: ResponseFormatType,
199}
200/// Supported response format types.
201#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
202#[serde(rename_all = "snake_case")]
203pub(crate) enum ResponseFormatType {
204    Text,
205    JsonObject,
206}
207
208impl ResponseFormat {
209    pub fn text() -> Self {
210        ResponseFormat {
211            typ: ResponseFormatType::Text,
212        }
213    }
214
215    pub fn json_object() -> Self {
216        ResponseFormat {
217            typ: ResponseFormatType::JsonObject,
218        }
219    }
220}
221
222/// Stop sequences for generation.
223#[non_exhaustive]
224#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
225#[serde(untagged)]
226pub enum Stop {
227    One(String),
228    Many(Vec<String>),
229}
230
231impl Stop {
232    fn is_empty(&self) -> bool {
233        match self {
234            Stop::One(value) => value.is_empty(),
235            Stop::Many(values) => values.is_empty(),
236        }
237    }
238}
239
240impl From<String> for Stop {
241    fn from(value: String) -> Self {
242        Stop::One(value)
243    }
244}
245
246impl From<&str> for Stop {
247    fn from(value: &str) -> Self {
248        Stop::One(value.to_string())
249    }
250}
251
252impl<T> From<Vec<T>> for Stop
253where
254    T: Into<String>,
255{
256    fn from(values: Vec<T>) -> Self {
257        Stop::Many(values.into_iter().map(Into::into).collect())
258    }
259}
260/// Streaming options for SSE responses.
261#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
262pub struct StreamOptions {
263    /// If set, an additional chunk will be streamed before the \`data: \[DONE\]\` message.
264    /// The `usage` field on this chunk shows the token usage statistics for the entire request,
265    /// and the `choices` field will always be an empty array.
266    /// All other chunks will also include a `usage` field, but with a null value.
267    pub include_usage: bool,
268}
269/// Tool definition used by the model.
270#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
271pub struct Tool {
272    /// The type of the tool. Currently, only `function` is supported.
273    #[serde(rename = "type")]
274    pub typ: ToolType,
275    pub function: ToolFunctionDefinition,
276}
277
278impl Tool {
279    pub fn new(
280        name: impl Into<String>,
281        description: impl Into<String>,
282        parameters: Option<serde_json::Value>,
283    ) -> Self {
284        Tool {
285            typ: ToolType::Function,
286            function: ToolFunctionDefinition {
287                name: name.into(),
288                description: description.into(),
289                parameters,
290            },
291        }
292    }
293}
294
295/// Tool type.
296#[non_exhaustive]
297#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
298#[serde(rename_all = "snake_case")]
299pub enum ToolType {
300    Function,
301}
302
303/// Tool function definition.
304#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
305pub struct ToolFunctionDefinition {
306    /// A description of what the function does,
307    /// used by the model to choose when and how to call the function.
308    pub description: String,
309    /// The name of the function to be called. Must be a-z, A-Z, 0-9,
310    /// or contain underscores and dashes, with a maximum length of 64.
311    pub name: String,
312    /// The parameters the functions accepts, described as a JSON Schema object.
313    /// See the [Tool Calls Guide](https://api-docs.deepseek.com/guides/tool_calls) for examples,
314    /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
315    ///
316    /// Omitting `parameters` defines a function with an empty parameter list.
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub parameters: Option<serde_json::Value>,
319}
320/// Tool choice configuration.
321#[non_exhaustive]
322#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
323#[serde(untagged)]
324pub enum ToolChoice {
325    /// Possible values: [`none`, `auto`, `required`]
326    Simple(ChatToolChoice),
327    /// {"type":"function","function":{...}}
328    Named(ChatNamedToolChoice),
329}
330
331impl ToolChoice {
332    pub fn named(function: serde_json::Value) -> Self {
333        ToolChoice::Named(ChatNamedToolChoice {
334            typ: ToolType::Function,
335            function,
336        })
337    }
338
339    pub fn none() -> Self {
340        ToolChoice::Simple(ChatToolChoice::None)
341    }
342
343    pub fn auto() -> Self {
344        ToolChoice::Simple(ChatToolChoice::Auto)
345    }
346
347    pub fn required() -> Self {
348        ToolChoice::Simple(ChatToolChoice::Required)
349    }
350}
351
352/// Tool choice values.
353#[non_exhaustive]
354#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
355#[serde(rename_all = "snake_case")]
356pub enum ChatToolChoice {
357    None,
358    Auto,
359    Required,
360}
361/// Named tool choice configuration.
362#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
363pub struct ChatNamedToolChoice {
364    /// Possible values: \`function\`
365    ///
366    /// The type of the tool. Currently, only `function` is supported.
367    #[serde(rename = "type")]
368    pub typ: ToolType,
369
370    pub function: serde_json::Value,
371}
372
373#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
374pub struct Thinking {
375    /// Possible values: [`enabled`, `disabled`]
376    ///
377    /// Default value: `enabled`
378    ///
379    /// If set to `enabled`, then use thinking mode. If set to `disabled`, then use non-thinking model.
380    #[serde(rename = "type")]
381    pub(crate) typ: ThinkingType,
382}
383
384impl Thinking {
385    pub fn enabled() -> Self {
386        Thinking {
387            typ: ThinkingType::Enabled,
388        }
389    }
390
391    pub fn disabled() -> Self {
392        Thinking {
393            typ: ThinkingType::Disabled,
394        }
395    }
396}
397
398#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
399#[serde(rename_all = "snake_case")]
400pub(crate) enum ThinkingType {
401    Enabled,
402    Disabled,
403}
404
405impl ChatRequestBuilder {
406    fn validate(&self) -> Result<(), String> {
407        // derive_builder + strip_option makes Option<T> fields become Option<Option<T>> here;
408        // flatten() treats "unset" and "explicit None" uniformly for validation.
409        if let Some(temperature) = self.temperature.flatten()
410            && !(0.0..=2.0).contains(&temperature)
411        {
412            return Err("temperature must be between 0 and 2".to_string());
413        }
414
415        if let Some(top_p) = self.top_p.flatten()
416            && !(0.0..=1.0).contains(&top_p)
417        {
418            return Err("top_p must be between 0 and 1".to_string());
419        }
420
421        if let Some(top_logprobs) = self.top_logprobs.flatten() {
422            if top_logprobs > 20 {
423                return Err("top_logprobs must be <= 20".to_string());
424            }
425            if self.logprobs.flatten() != Some(true) {
426                return Err("top_logprobs requires logprobs=true".to_string());
427            }
428        }
429
430        if let Some(stream) = self.stream.flatten()
431            && !stream
432            && self.stream_options.is_some()
433        {
434            return Err("stream_options cannot be set when stream is false".to_string());
435        }
436
437        if let Some(stop) = self.stop.as_ref().and_then(|s| s.as_ref())
438            && let Stop::Many(values) = stop
439            && values.len() > 16
440        {
441            return Err("a maximum of 16 stop sequences are allowed".to_string());
442        }
443
444        if let Some(user_id) = self.user_id.as_ref().and_then(|u| u.as_ref()) {
445            if user_id.len() > 512 {
446                return Err("user_id must be at most 512 characters".to_string());
447            }
448            if !user_id
449                .chars()
450                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
451            {
452                return Err("user_id must only contain [a-zA-Z0-9\\-_]".to_string());
453            }
454        }
455
456        Ok(())
457    }
458}