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}