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)]
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, 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        pub parameters: Option<serde_json::Value>,
584    }
585    /// Tool choice configuration.
586    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
587    #[serde(untagged)]
588    pub enum ToolChoice {
589        /// Possible values: [`none`, `auto`, r`equired]
590        Simple(ChatToolChoice),
591        /// {"type":"function","function":{...}}
592        Named(ChatNamedToolChoice),
593    }
594
595    impl ToolChoice {
596        pub fn named(function: serde_json::Value) -> Self {
597            ToolChoice::Named(ChatNamedToolChoice {
598                typ: ToolType::Function,
599                function,
600            })
601        }
602
603        pub fn none() -> Self {
604            ToolChoice::Simple(ChatToolChoice::None)
605        }
606
607        pub fn auto() -> Self {
608            ToolChoice::Simple(ChatToolChoice::Auto)
609        }
610
611        pub fn required() -> Self {
612            ToolChoice::Simple(ChatToolChoice::Required)
613        }
614    }
615
616    /// Tool choice values.
617    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
618    #[serde(rename_all = "snake_case")]
619    pub enum ChatToolChoice {
620        None,
621        Auto,
622        Required,
623    }
624    /// Named tool choice configuration.
625    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
626    pub struct ChatNamedToolChoice {
627        /// Possible values: [`function`]
628        ///
629        /// The type of the tool. Currently, only `function` is supported.
630        #[serde(rename = "type")]
631        pub typ: ToolType,
632
633        pub function: serde_json::Value,
634    }
635
636    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
637    pub struct Thinking {
638        /// Possible values: [`enabled`, `disabled`]
639        ///
640        /// Default value: `enabled`
641        ///
642        /// If set to `enabled`, then use thinking mode. If set to `disabled`, then use non-thinking model.
643        #[serde(rename = "type")]
644        pub(crate) typ: ThinkingType,
645    }
646
647    impl Thinking {
648        pub fn enabled() -> Self {
649            Thinking {
650                typ: ThinkingType::Enabled,
651            }
652        }
653
654        pub fn disabled() -> Self {
655            Thinking {
656                typ: ThinkingType::Disabled,
657            }
658        }
659    }
660
661    #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
662    #[serde(rename_all = "snake_case")]
663    pub(crate) enum ThinkingType {
664        Enabled,
665        Disabled,
666    }
667
668    impl ChatRequestBuilder {
669        fn validate(&self) -> Result<(), String> {
670            // derive_builder + strip_option makes Option<T> fields become Option<Option<T>> here;
671            // flatten() treats "unset" and "explicit None" uniformly for validation.
672            if let Some(temperature) = self.temperature.flatten() {
673                if !(0.0..=2.0).contains(&temperature) {
674                    return Err("temperature must be between 0 and 2".to_string());
675                }
676            }
677
678            if let Some(top_p) = self.top_p.flatten() {
679                if !(0.0..=1.0).contains(&top_p) {
680                    return Err("top_p must be between 0 and 1".to_string());
681                }
682            }
683
684            if let Some(top_logprobs) = self.top_logprobs.flatten() {
685                if top_logprobs > 20 {
686                    return Err("top_logprobs must be <= 20".to_string());
687                }
688                if self.logprobs.flatten() != Some(true) {
689                    return Err("top_logprobs requires logprobs=true".to_string());
690                }
691            }
692
693            if let Some(thinking) = self
694                .thinking
695                .as_ref()
696                .and_then(|thinking| thinking.as_ref())
697            {
698                if let Some(reasoning_effort) = self
699                    .reasoning_effort
700                    .as_ref()
701                    .and_then(|effort| effort.as_ref())
702                {
703                    if matches!(thinking.typ, ThinkingType::Disabled)
704                        && matches!(
705                            reasoning_effort,
706                            ReasoningEffort::High | ReasoningEffort::Max
707                        )
708                    {
709                        return Err(
710                            "thinking options type cannot be disabled when reasoning_effort is set"
711                                .to_string(),
712                        );
713                    }
714                }
715            }
716
717            if let Some(stream) = self.stream.flatten() {
718                if !stream && self.stream_options.is_some() {
719                    return Err("stream_options cannot be set when stream is false".to_string());
720                }
721            }
722
723            if let Some(stop) = self.stop.as_ref().and_then(|s| s.as_ref()) {
724                if let Stop::Many(values) = stop {
725                    if values.len() > 16 {
726                        return Err("a maximum of 16 stop sequences are allowed".to_string());
727                    }
728                }
729            }
730
731            Ok(())
732        }
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::request::*;
739    use super::response::*;
740    use serde_json::{Value, json};
741
742    #[test]
743    fn response_format_serializes_to_json_object() {
744        let format = ResponseFormat::json_object();
745        let value = serde_json::to_value(format).unwrap();
746        assert_eq!(value, json!({"type": "json_object"}));
747    }
748
749    #[test]
750    fn stop_supports_string_and_array() {
751        let single = Stop::from("END");
752        let many = Stop::from(vec!["END", "STOP"]);
753
754        let single_value = serde_json::to_value(single).unwrap();
755        let many_value = serde_json::to_value(many).unwrap();
756
757        assert_eq!(single_value, json!("END"));
758        assert_eq!(many_value, json!(["END", "STOP"]));
759
760        let single_back: Stop = serde_json::from_value(json!("END")).unwrap();
761        let many_back: Stop = serde_json::from_value(json!(["A", "B"])).unwrap();
762        assert!(matches!(single_back, Stop::One(_)));
763        assert!(matches!(many_back, Stop::Many(_)));
764
765        let none_back: Option<Stop> = serde_json::from_value(Value::Null).unwrap();
766        assert!(none_back.is_none());
767    }
768
769    #[test]
770    fn tool_choice_serializes_simple_and_named() {
771        let simple = ToolChoice::Simple(ChatToolChoice::Auto);
772        let simple_value = serde_json::to_value(simple).unwrap();
773        assert_eq!(simple_value, json!("auto"));
774
775        let named = ToolChoice::named(json!({"name": "get_weather"}));
776        let named_value = serde_json::to_value(named).unwrap();
777        assert_eq!(
778            named_value,
779            json!({"type": "function", "function": {"name": "get_weather"}})
780        );
781    }
782
783    #[test]
784    fn chat_message_serializes_role_and_omits_prefix_by_default() {
785        let message = ChatMessage::Assistant {
786            content: Some("Hello".to_string()),
787            name: None,
788            tool_calls: None,
789        };
790        let value = serde_json::to_value(message).unwrap();
791        assert_eq!(value.get("role"), Some(&json!("assistant")));
792        assert_eq!(value.get("content"), Some(&json!("Hello")));
793        assert!(value.get("reasoning_content").is_none());
794    }
795
796    #[test]
797    fn response_tool_call_type_serializes_as_function() {
798        let call = ToolCall::new("call_i", "get_weather", "{}");
799        let value = serde_json::to_value(call).unwrap();
800        assert_eq!(value.get("type"), Some(&json!("function")));
801    }
802
803    #[test]
804    fn builder_validation_rejects_out_of_range_values() {
805        fn base_builder() -> ChatRequestBuilder {
806            ChatRequestBuilder::default()
807                .model("deepseek-v4-pro")
808                .message(ChatMessage::User {
809                    content: "Hi".to_string(),
810                    name: None,
811                })
812        }
813
814        let too_hot = base_builder().temperature(2.5).build();
815        assert!(too_hot.is_err());
816
817        let bad_top_p = base_builder().top_p(1.1).build();
818        assert!(bad_top_p.is_err());
819
820        let bad_top_logprobs = base_builder().top_logprobs(21_u32).logprobs(true).build();
821        assert!(bad_top_logprobs.is_err());
822
823        let missing_logprobs = base_builder().top_logprobs(2_u32).build();
824        assert!(missing_logprobs.is_err());
825    }
826
827    #[test]
828    fn thinking_struct_serializes_type() {
829        let thinking = Thinking::disabled();
830        let value = serde_json::to_value(&thinking).unwrap();
831        assert_eq!(value.get("type"), Some(&json!("disabled")));
832
833        let req = ChatRequestBuilder::default()
834            .model("deepseek-v4-flash")
835            .message(ChatMessage::User {
836                content: "Hi".to_string(),
837                name: None,
838            })
839            .thinking(thinking)
840            .reasoning_effort(ReasoningEffort::Max)
841            .build();
842        // thinking options type cannot be disabled when reasoning_effort is set
843        assert!(req.is_err());
844    }
845}