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`, `deepseek-v4-flash-vision-exp`\]
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. Either a plain string, or an array of content parts
161        /// (for image input with the `deepseek-v4-flash-vision-exp` model).
162        content: UserContent,
163        /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
164        #[serde(skip_serializing_if = "Option::is_none")]
165        name: Option<String>,
166    },
167    Assistant {
168        /// The contents of the assistant message.
169        #[serde(skip_serializing_if = "Option::is_none")]
170        content: Option<String>,
171        /// An optional name for the participant. Provides the model information to differentiate between participants of the same role.
172        #[serde(skip_serializing_if = "Option::is_none")]
173        name: Option<String>,
174
175        #[serde(skip_serializing_if = "super::is_none_or_empty_vec")]
176        tool_calls: Option<Vec<super::response::ToolCall>>,
177    },
178    Tool {
179        /// The contents of the tool message.
180        content: String,
181        /// Tool call that this message is responding to.
182        tool_call_id: String,
183    },
184}
185
186/// Content of a user message.
187///
188/// Either a plain string or an array of content parts (for multimodal input).
189#[non_exhaustive]
190#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
191#[serde(untagged)]
192pub enum UserContent {
193    /// Plain text content.
194    Text(String),
195    /// Array of content parts (text, image, file).
196    Parts(Vec<UserContentPart>),
197}
198
199impl UserContent {
200    /// Create a plain text content.
201    pub fn text(s: impl Into<String>) -> Self {
202        UserContent::Text(s.into())
203    }
204
205    /// Create content from a list of parts.
206    pub fn parts(parts: Vec<UserContentPart>) -> Self {
207        UserContent::Parts(parts)
208    }
209
210    /// Create content with a single image URL.
211    pub fn image_url(url: impl Into<String>) -> Self {
212        UserContent::Parts(vec![UserContentPart::image_url(url)])
213    }
214
215    /// Create content with a single image URL and detail level.
216    pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
217        UserContent::Parts(vec![UserContentPart::image_url_with_detail(url, detail)])
218    }
219
220    /// Create content referencing an uploaded file by ID.
221    pub fn file_id(id: impl Into<String>) -> Self {
222        UserContent::Parts(vec![UserContentPart::file_id(id)])
223    }
224
225    /// Create content with an inline base64 image.
226    pub fn file_data(data: impl Into<String>, filename: impl Into<String>) -> Self {
227        UserContent::Parts(vec![UserContentPart::file_data(data, filename)])
228    }
229}
230
231impl From<String> for UserContent {
232    fn from(s: String) -> Self {
233        UserContent::Text(s)
234    }
235}
236
237impl From<&str> for UserContent {
238    fn from(s: &str) -> Self {
239        UserContent::Text(s.to_string())
240    }
241}
242
243impl From<Vec<UserContentPart>> for UserContent {
244    fn from(parts: Vec<UserContentPart>) -> Self {
245        UserContent::Parts(parts)
246    }
247}
248
249/// A content part within a multimodal user message.
250#[non_exhaustive]
251#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
252#[serde(tag = "type", rename_all = "snake_case")]
253pub enum UserContentPart {
254    /// Text content part.
255    Text {
256        /// The text content.
257        text: String,
258    },
259    /// Image content part (URL or base64).
260    ImageUrl {
261        /// Image URL details.
262        image_url: ImageUrlDetail,
263    },
264    /// File content part (uploaded file ID or inline base64).
265    File {
266        /// The ID of a file uploaded via the Files API.
267        #[serde(skip_serializing_if = "Option::is_none")]
268        file_id: Option<String>,
269        /// A base64-encoded data URL of the image.
270        #[serde(skip_serializing_if = "Option::is_none")]
271        file_data: Option<String>,
272        /// An optional filename (only valid with `file_data`).
273        #[serde(skip_serializing_if = "Option::is_none")]
274        filename: Option<String>,
275    },
276}
277
278impl UserContentPart {
279    /// Create a text content part.
280    pub fn text(s: impl Into<String>) -> Self {
281        UserContentPart::Text { text: s.into() }
282    }
283
284    /// Create an image URL content part.
285    pub fn image_url(url: impl Into<String>) -> Self {
286        UserContentPart::ImageUrl {
287            image_url: ImageUrlDetail {
288                url: url.into(),
289                detail: None,
290            },
291        }
292    }
293
294    /// Create an image URL content part with detail level.
295    pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
296        UserContentPart::ImageUrl {
297            image_url: ImageUrlDetail {
298                url: url.into(),
299                detail: Some(detail),
300            },
301        }
302    }
303
304    /// Create a file content part referencing an uploaded file.
305    pub fn file_id(id: impl Into<String>) -> Self {
306        UserContentPart::File {
307            file_id: Some(id.into()),
308            file_data: None,
309            filename: None,
310        }
311    }
312
313    /// Create a file content part with inline base64 data.
314    pub fn file_data(data: impl Into<String>, filename: impl Into<String>) -> Self {
315        UserContentPart::File {
316            file_id: None,
317            file_data: Some(data.into()),
318            filename: Some(filename.into()),
319        }
320    }
321}
322
323/// Image URL detail for vision requests.
324#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
325pub struct ImageUrlDetail {
326    /// The image URL (http(s) URL or base64 data URL).
327    pub url: String,
328    /// Controls how the image is processed.
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub detail: Option<ImageDetail>,
331}
332
333/// Image detail level.
334#[non_exhaustive]
335#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
336#[serde(rename_all = "snake_case")]
337pub enum ImageDetail {
338    /// Downscale to 512x512 (faster, cheaper).
339    Low,
340    /// Process at high resolution with image tiling (slower, more detail).
341    High,
342    /// Use the original resolution without downscaling.
343    Original,
344    /// Let the model choose the processing level.
345    Auto,
346}
347
348/// Reasoning effort hints for the model.
349#[non_exhaustive]
350#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
351#[serde(rename_all = "snake_case")]
352pub enum ReasoningEffort {
353    High,
354    Max,
355}
356/// Response format configuration.
357#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
358pub struct ResponseFormat {
359    /// Default value: `text`
360    /// Must be one of `text` or `json_object`.
361    #[serde(rename = "type")]
362    pub(crate) typ: ResponseFormatType,
363}
364/// Supported response format types.
365#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
366#[serde(rename_all = "snake_case")]
367pub(crate) enum ResponseFormatType {
368    Text,
369    JsonObject,
370}
371
372impl ResponseFormat {
373    pub fn text() -> Self {
374        ResponseFormat {
375            typ: ResponseFormatType::Text,
376        }
377    }
378
379    pub fn json_object() -> Self {
380        ResponseFormat {
381            typ: ResponseFormatType::JsonObject,
382        }
383    }
384}
385
386/// Stop sequences for generation.
387#[non_exhaustive]
388#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
389#[serde(untagged)]
390pub enum Stop {
391    One(String),
392    Many(Vec<String>),
393}
394
395impl Stop {
396    fn is_empty(&self) -> bool {
397        match self {
398            Stop::One(value) => value.is_empty(),
399            Stop::Many(values) => values.is_empty(),
400        }
401    }
402}
403
404impl From<String> for Stop {
405    fn from(value: String) -> Self {
406        Stop::One(value)
407    }
408}
409
410impl From<&str> for Stop {
411    fn from(value: &str) -> Self {
412        Stop::One(value.to_string())
413    }
414}
415
416impl<T> From<Vec<T>> for Stop
417where
418    T: Into<String>,
419{
420    fn from(values: Vec<T>) -> Self {
421        Stop::Many(values.into_iter().map(Into::into).collect())
422    }
423}
424/// Streaming options for SSE responses.
425#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
426pub struct StreamOptions {
427    /// If set, an additional chunk will be streamed before the \`data: \[DONE\]\` message.
428    /// The `usage` field on this chunk shows the token usage statistics for the entire request,
429    /// and the `choices` field will always be an empty array.
430    /// All other chunks will also include a `usage` field, but with a null value.
431    pub include_usage: bool,
432}
433/// Tool definition used by the model.
434#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
435pub struct Tool {
436    /// The type of the tool. Currently, only `function` is supported.
437    #[serde(rename = "type")]
438    pub typ: ToolType,
439    pub function: ToolFunctionDefinition,
440}
441
442impl Tool {
443    pub fn new(
444        name: impl Into<String>,
445        description: impl Into<String>,
446        parameters: Option<serde_json::Value>,
447    ) -> Self {
448        Tool {
449            typ: ToolType::Function,
450            function: ToolFunctionDefinition {
451                name: name.into(),
452                description: description.into(),
453                parameters,
454            },
455        }
456    }
457}
458
459/// Tool type.
460#[non_exhaustive]
461#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
462#[serde(rename_all = "snake_case")]
463pub enum ToolType {
464    Function,
465}
466
467/// Tool function definition.
468#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
469pub struct ToolFunctionDefinition {
470    /// A description of what the function does,
471    /// used by the model to choose when and how to call the function.
472    pub description: String,
473    /// The name of the function to be called. Must be a-z, A-Z, 0-9,
474    /// or contain underscores and dashes, with a maximum length of 64.
475    pub name: String,
476    /// The parameters the functions accepts, described as a JSON Schema object.
477    /// See the [Tool Calls Guide](https://api-docs.deepseek.com/guides/tool_calls) for examples,
478    /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
479    ///
480    /// Omitting `parameters` defines a function with an empty parameter list.
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub parameters: Option<serde_json::Value>,
483}
484/// Tool choice configuration.
485#[non_exhaustive]
486#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
487#[serde(untagged)]
488pub enum ToolChoice {
489    /// Possible values: [`none`, `auto`, `required`]
490    Simple(ChatToolChoice),
491    /// {"type":"function","function":{...}}
492    Named(ChatNamedToolChoice),
493}
494
495impl ToolChoice {
496    pub fn named(function: serde_json::Value) -> Self {
497        ToolChoice::Named(ChatNamedToolChoice {
498            typ: ToolType::Function,
499            function,
500        })
501    }
502
503    pub fn none() -> Self {
504        ToolChoice::Simple(ChatToolChoice::None)
505    }
506
507    pub fn auto() -> Self {
508        ToolChoice::Simple(ChatToolChoice::Auto)
509    }
510
511    pub fn required() -> Self {
512        ToolChoice::Simple(ChatToolChoice::Required)
513    }
514}
515
516/// Tool choice values.
517#[non_exhaustive]
518#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
519#[serde(rename_all = "snake_case")]
520pub enum ChatToolChoice {
521    None,
522    Auto,
523    Required,
524}
525/// Named tool choice configuration.
526#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
527pub struct ChatNamedToolChoice {
528    /// Possible values: \`function\`
529    ///
530    /// The type of the tool. Currently, only `function` is supported.
531    #[serde(rename = "type")]
532    pub typ: ToolType,
533
534    pub function: serde_json::Value,
535}
536
537#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
538pub struct Thinking {
539    /// Possible values: [`enabled`, `disabled`]
540    ///
541    /// Default value: `enabled`
542    ///
543    /// If set to `enabled`, then use thinking mode. If set to `disabled`, then use non-thinking model.
544    #[serde(rename = "type")]
545    pub(crate) typ: ThinkingType,
546}
547
548impl Thinking {
549    pub fn enabled() -> Self {
550        Thinking {
551            typ: ThinkingType::Enabled,
552        }
553    }
554
555    pub fn disabled() -> Self {
556        Thinking {
557            typ: ThinkingType::Disabled,
558        }
559    }
560}
561
562#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
563#[serde(rename_all = "snake_case")]
564pub(crate) enum ThinkingType {
565    Enabled,
566    Disabled,
567}
568
569impl ChatRequestBuilder {
570    fn validate(&self) -> Result<(), String> {
571        // derive_builder + strip_option makes Option<T> fields become Option<Option<T>> here;
572        // flatten() treats "unset" and "explicit None" uniformly for validation.
573        if let Some(temperature) = self.temperature.flatten()
574            && !(0.0..=2.0).contains(&temperature)
575        {
576            return Err("temperature must be between 0 and 2".to_string());
577        }
578
579        if let Some(top_p) = self.top_p.flatten()
580            && !(0.0..=1.0).contains(&top_p)
581        {
582            return Err("top_p must be between 0 and 1".to_string());
583        }
584
585        if let Some(top_logprobs) = self.top_logprobs.flatten() {
586            if top_logprobs > 20 {
587                return Err("top_logprobs must be <= 20".to_string());
588            }
589            if self.logprobs.flatten() != Some(true) {
590                return Err("top_logprobs requires logprobs=true".to_string());
591            }
592        }
593
594        if let Some(stream) = self.stream.flatten()
595            && !stream
596            && self.stream_options.is_some()
597        {
598            return Err("stream_options cannot be set when stream is false".to_string());
599        }
600
601        if let Some(stop) = self.stop.as_ref().and_then(|s| s.as_ref())
602            && let Stop::Many(values) = stop
603            && values.len() > 16
604        {
605            return Err("a maximum of 16 stop sequences are allowed".to_string());
606        }
607
608        if let Some(user_id) = self.user_id.as_ref().and_then(|u| u.as_ref()) {
609            if user_id.len() > 512 {
610                return Err("user_id must be at most 512 characters".to_string());
611            }
612            if !user_id
613                .chars()
614                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
615            {
616                return Err("user_id must only contain [a-zA-Z0-9\\-_]".to_string());
617            }
618        }
619
620        Ok(())
621    }
622}