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#[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 #[builder(setter(each(name = "message", into)))]
22 pub messages: Vec<ChatMessage>,
23
24 pub model: String,
28
29 #[builder(default)]
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub thinking: Option<Thinking>,
33
34 #[builder(default)]
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub reasoning_effort: Option<ReasoningEffort>,
45
46 #[builder(default)]
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub max_tokens: Option<u32>,
54
55 #[builder(default)]
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub response_format: Option<ResponseFormat>,
64
65 #[builder(default)]
67 #[serde(skip_serializing_if = "is_none_or_empty_stop")]
68 pub stop: Option<Stop>,
69
70 #[builder(default)]
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub stream: Option<bool>,
76
77 #[builder(default)]
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub stream_options: Option<StreamOptions>,
81
82 #[builder(default)]
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub temperature: Option<f64>,
91
92 #[builder(default)]
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub top_p: Option<f64>,
104
105 #[builder(default, setter(each(name = "tool", into)))]
109 #[serde(skip_serializing_if = "Vec::is_empty")]
110 pub tools: Vec<Tool>,
111
112 #[builder(default)]
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub tool_choice: Option<ToolChoice>,
121
122 #[builder(default)]
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub logprobs: Option<bool>,
127
128 #[builder(default)]
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub top_logprobs: Option<u32>,
135
136 #[builder(default)]
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub user_id: Option<String>,
146}
147#[non_exhaustive]
149#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
150#[serde(tag = "role", rename_all = "snake_case")]
151pub enum ChatMessage {
152 System {
153 content: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 name: Option<String>,
158 },
159 User {
160 content: UserContent,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 name: Option<String>,
166 },
167 Assistant {
168 #[serde(skip_serializing_if = "Option::is_none")]
170 content: Option<String>,
171 #[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 content: String,
181 tool_call_id: String,
183 },
184}
185
186#[non_exhaustive]
190#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
191#[serde(untagged)]
192pub enum UserContent {
193 Text(String),
195 Parts(Vec<UserContentPart>),
197}
198
199impl UserContent {
200 pub fn text(s: impl Into<String>) -> Self {
202 UserContent::Text(s.into())
203 }
204
205 pub fn parts(parts: Vec<UserContentPart>) -> Self {
207 UserContent::Parts(parts)
208 }
209
210 pub fn image_url(url: impl Into<String>) -> Self {
212 UserContent::Parts(vec![UserContentPart::image_url(url)])
213 }
214
215 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 pub fn file_id(id: impl Into<String>) -> Self {
222 UserContent::Parts(vec![UserContentPart::file_id(id)])
223 }
224
225 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#[non_exhaustive]
251#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
252#[serde(tag = "type", rename_all = "snake_case")]
253pub enum UserContentPart {
254 Text {
256 text: String,
258 },
259 ImageUrl {
261 image_url: ImageUrlDetail,
263 },
264 File {
266 #[serde(skip_serializing_if = "Option::is_none")]
268 file_id: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 file_data: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 filename: Option<String>,
275 },
276}
277
278impl UserContentPart {
279 pub fn text(s: impl Into<String>) -> Self {
281 UserContentPart::Text { text: s.into() }
282 }
283
284 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
325pub struct ImageUrlDetail {
326 pub url: String,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub detail: Option<ImageDetail>,
331}
332
333#[non_exhaustive]
335#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
336#[serde(rename_all = "snake_case")]
337pub enum ImageDetail {
338 Low,
340 High,
342 Original,
344 Auto,
346}
347
348#[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#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
358pub struct ResponseFormat {
359 #[serde(rename = "type")]
362 pub(crate) typ: ResponseFormatType,
363}
364#[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#[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#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
426pub struct StreamOptions {
427 pub include_usage: bool,
432}
433#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
435pub struct Tool {
436 #[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#[non_exhaustive]
461#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
462#[serde(rename_all = "snake_case")]
463pub enum ToolType {
464 Function,
465}
466
467#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
469pub struct ToolFunctionDefinition {
470 pub description: String,
473 pub name: String,
476 #[serde(skip_serializing_if = "Option::is_none")]
482 pub parameters: Option<serde_json::Value>,
483}
484#[non_exhaustive]
486#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
487#[serde(untagged)]
488pub enum ToolChoice {
489 Simple(ChatToolChoice),
491 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#[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#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
527pub struct ChatNamedToolChoice {
528 #[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 #[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 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}