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}