1use super::*;
2use derive_builder::Builder;
3
4#[derive(Clone, Debug, PartialEq, Serialize, Builder)]
6#[builder(
7 pattern = "owned",
8 setter(into, strip_option),
9 build_fn(validate = "Self::validate"),
10 name = "ResponsesRequestBuilder"
11)]
12pub struct ResponsesRequest {
13 #[serde(skip_serializing)]
14 pub client: DeepSeekClient,
15
16 pub model: String,
18
19 #[builder(default)]
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub input: Option<Input>,
26
27 #[builder(default)]
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub instructions: Option<String>,
31
32 #[builder(default)]
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub reasoning: Option<Reasoning>,
36
37 #[builder(default)]
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub max_output_tokens: Option<u32>,
42
43 #[builder(default)]
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub stream: Option<bool>,
49
50 #[builder(default)]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub temperature: Option<f64>,
60
61 #[builder(default)]
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub top_p: Option<f64>,
70
71 #[builder(default)]
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub text: Option<Text>,
75
76 #[builder(default, setter(each(name = "tool", into)))]
80 #[serde(skip_serializing_if = "Vec::is_empty")]
81 pub tools: Vec<Tool>,
82
83 #[builder(default)]
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub tool_choice: Option<ToolChoice>,
94
95 #[builder(default)]
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub top_logprobs: Option<u32>,
102
103 #[builder(default)]
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub user: Option<String>,
108}
109
110#[derive(Clone, Debug, PartialEq, Serialize)]
113#[serde(untagged)]
114pub enum Input {
115 TextInput(String),
117 InputItemList(Vec<InputItem>),
119}
120
121impl From<String> for Input {
122 fn from(value: String) -> Self {
123 Input::TextInput(value)
124 }
125}
126
127impl From<&str> for Input {
128 fn from(value: &str) -> Self {
129 Input::TextInput(value.to_string())
130 }
131}
132
133impl From<Vec<InputItem>> for Input {
134 fn from(value: Vec<InputItem>) -> Self {
135 Input::InputItemList(value)
136 }
137}
138
139#[derive(Clone, Debug, PartialEq, Serialize)]
141pub struct InputItem {
142 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
144 pub typ: Option<InputItemType>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub role: Option<InputRole>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
153 pub content: Option<InputContent>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub call_id: Option<String>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub name: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub arguments: Option<String>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub output: Option<String>,
170}
171
172impl InputItem {
173 pub fn user(content: impl Into<String>) -> Self {
175 InputItem {
176 typ: None,
177 role: Some(InputRole::User),
178 content: Some(InputContent::Text(content.into())),
179 call_id: None,
180 name: None,
181 arguments: None,
182 output: None,
183 }
184 }
185
186 pub fn assistant(content: impl Into<String>) -> Self {
188 InputItem {
189 typ: None,
190 role: Some(InputRole::Assistant),
191 content: Some(InputContent::Text(content.into())),
192 call_id: None,
193 name: None,
194 arguments: None,
195 output: None,
196 }
197 }
198
199 pub fn function_call(
201 call_id: impl Into<String>,
202 name: impl Into<String>,
203 arguments: impl Into<String>,
204 ) -> Self {
205 InputItem {
206 typ: Some(InputItemType::FunctionCall),
207 role: None,
208 content: None,
209 call_id: Some(call_id.into()),
210 name: Some(name.into()),
211 arguments: Some(arguments.into()),
212 output: None,
213 }
214 }
215
216 pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
218 InputItem {
219 typ: Some(InputItemType::FunctionCallOutput),
220 role: None,
221 content: None,
222 call_id: Some(call_id.into()),
223 name: None,
224 arguments: None,
225 output: Some(output.into()),
226 }
227 }
228}
229
230#[non_exhaustive]
232#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
233#[serde(rename_all = "snake_case")]
234pub enum InputItemType {
235 Message,
236 FunctionCall,
237 FunctionCallOutput,
238 Reasoning,
239 WebSearchCall,
240 #[serde(other)]
242 Unknown,
243}
244
245#[non_exhaustive]
247#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
248#[serde(rename_all = "snake_case")]
249pub enum InputRole {
250 User,
251 Assistant,
252 System,
253 Developer,
255 #[serde(other)]
256 Unknown,
257}
258
259#[derive(Clone, Debug, PartialEq, Serialize)]
261#[serde(untagged)]
262pub enum InputContent {
263 Text(String),
265 Parts(Vec<InputContentPart>),
267}
268
269impl From<String> for InputContent {
270 fn from(value: String) -> Self {
271 InputContent::Text(value)
272 }
273}
274
275impl From<&str> for InputContent {
276 fn from(value: &str) -> Self {
277 InputContent::Text(value.to_string())
278 }
279}
280
281#[derive(Clone, Debug, PartialEq, Serialize)]
283#[serde(tag = "type", rename_all = "snake_case")]
284pub enum InputContentPart {
285 InputText { text: String },
286 OutputText { text: String },
287}
288
289#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
291pub struct Reasoning {
292 pub effort: ReasoningEffort,
294}
295
296impl Reasoning {
297 pub fn new(effort: ReasoningEffort) -> Self {
298 Reasoning { effort }
299 }
300}
301
302#[non_exhaustive]
304#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
305#[serde(rename_all = "snake_case")]
306pub enum ReasoningEffort {
307 None,
309 Minimal,
310 Low,
311 Medium,
312 High,
313 #[serde(rename = "xhigh")]
315 XHigh,
316 #[serde(rename = "max")]
318 Max,
319}
320
321#[derive(Clone, Debug, PartialEq, Serialize)]
323pub struct Text {
324 pub format: TextFormat,
326}
327
328impl Text {
329 pub fn new(format: TextFormat) -> Self {
330 Text { format }
331 }
332}
333
334#[derive(Clone, Debug, PartialEq, Serialize)]
336#[serde(tag = "type", rename_all = "snake_case")]
337pub enum TextFormat {
338 Text,
340 JsonObject,
342 JsonSchema {
344 name: String,
346 schema: serde_json::Value,
348 },
349}
350
351impl TextFormat {
352 pub fn text() -> Self {
353 TextFormat::Text
354 }
355
356 pub fn json_object() -> Self {
357 TextFormat::JsonObject
358 }
359
360 pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
361 TextFormat::JsonSchema {
362 name: name.into(),
363 schema,
364 }
365 }
366}
367
368#[derive(Clone, Debug, PartialEq, Serialize)]
370pub struct Tool {
371 #[serde(rename = "type")]
373 pub typ: ToolType,
374
375 #[serde(skip_serializing_if = "Option::is_none")]
378 pub name: Option<String>,
379
380 #[serde(skip_serializing_if = "Option::is_none")]
382 pub description: Option<String>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
388 pub parameters: Option<serde_json::Value>,
389}
390
391impl Tool {
392 pub fn function(
394 name: impl Into<String>,
395 description: impl Into<String>,
396 parameters: Option<serde_json::Value>,
397 ) -> Self {
398 Tool {
399 typ: ToolType::Function,
400 name: Some(name.into()),
401 description: Some(description.into()),
402 parameters,
403 }
404 }
405
406 pub fn web_search() -> Self {
408 Tool {
409 typ: ToolType::WebSearch,
410 name: None,
411 description: None,
412 parameters: None,
413 }
414 }
415}
416
417#[non_exhaustive]
419#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
420#[serde(rename_all = "snake_case")]
421pub enum ToolType {
422 Function,
423 WebSearch,
424 #[serde(rename = "web_search_2025_08_26")]
425 WebSearch2025_08_26,
426}
427
428#[non_exhaustive]
430#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
431#[serde(untagged)]
432pub enum ToolChoice {
433 Mode(ToolChoiceMode),
435 Named(NamedToolChoice),
437}
438
439impl ToolChoice {
440 pub fn none() -> Self {
441 ToolChoice::Mode(ToolChoiceMode::None)
442 }
443
444 pub fn auto() -> Self {
445 ToolChoice::Mode(ToolChoiceMode::Auto)
446 }
447
448 pub fn required() -> Self {
449 ToolChoice::Mode(ToolChoiceMode::Required)
450 }
451
452 pub fn named(name: impl Into<String>) -> Self {
453 ToolChoice::Named(NamedToolChoice {
454 typ: ToolType::Function,
455 name: Some(name.into()),
456 })
457 }
458
459 pub fn web_search() -> Self {
460 ToolChoice::Named(NamedToolChoice {
461 typ: ToolType::WebSearch,
462 name: None,
463 })
464 }
465}
466
467#[non_exhaustive]
469#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
470#[serde(rename_all = "snake_case")]
471pub enum ToolChoiceMode {
472 None,
473 Auto,
474 Required,
475}
476
477#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
479pub struct NamedToolChoice {
480 #[serde(rename = "type")]
482 pub typ: ToolType,
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub name: Option<String>,
486}
487
488impl ResponsesRequestBuilder {
489 fn validate(&self) -> Result<(), String> {
490 if self.input.as_ref().and_then(|o| o.as_ref()).is_none()
491 && self
492 .instructions
493 .as_ref()
494 .and_then(|o| o.as_ref())
495 .is_none()
496 {
497 return Err("at least one of `input` and `instructions` is required".to_string());
498 }
499
500 if let Some(temperature) = self.temperature.flatten()
501 && !(0.0..=2.0).contains(&temperature)
502 {
503 return Err("temperature must be between 0 and 2".to_string());
504 }
505
506 if let Some(top_p) = self.top_p.flatten()
507 && !(0.0..=1.0).contains(&top_p)
508 {
509 return Err("top_p must be between 0 and 1".to_string());
510 }
511
512 if let Some(top_logprobs) = self.top_logprobs.flatten()
513 && top_logprobs > 20
514 {
515 return Err("top_logprobs must be <= 20".to_string());
516 }
517
518 if let Some(user) = self.user.as_ref().and_then(|u| u.as_ref()) {
519 if user.len() > 512 {
520 return Err("user must be at most 512 characters".to_string());
521 }
522 if !user
523 .chars()
524 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
525 {
526 return Err("user must only contain [a-zA-Z0-9\\-_]".to_string());
527 }
528 }
529
530 Ok(())
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use serde_json::json;
538
539 fn client() -> DeepSeekClient {
540 DeepSeekClient::new(
541 std::env::var("DEEPSEEK_API_KEY").expect("DEEPSEEK_API_KEY is not set"),
542 crate::DEFAULT_BASE_URL.clone(),
543 )
544 }
545
546 #[test]
547 fn input_serializes_as_string_or_list() {
548 let text = Input::TextInput("Hi".to_string());
549 assert_eq!(serde_json::to_value(text).unwrap(), json!("Hi"));
550
551 let items = Input::InputItemList(vec![InputItem::user("Hi")]);
552 assert_eq!(
553 serde_json::to_value(items).unwrap(),
554 json!([{"role": "user", "content": "Hi"}])
555 );
556 }
557
558 #[test]
559 fn reasoning_effort_serializes_effort_values() {
560 assert_eq!(
561 serde_json::to_value(ReasoningEffort::None).unwrap(),
562 json!("none")
563 );
564 assert_eq!(
565 serde_json::to_value(ReasoningEffort::XHigh).unwrap(),
566 json!("xhigh")
567 );
568 assert_eq!(
569 serde_json::to_value(ReasoningEffort::Max).unwrap(),
570 json!("max")
571 );
572 }
573
574 #[test]
575 fn tool_type_serializes_web_search_names() {
576 assert_eq!(
577 serde_json::to_value(ToolType::WebSearch).unwrap(),
578 json!("web_search")
579 );
580 assert_eq!(
581 serde_json::to_value(ToolType::WebSearch2025_08_26).unwrap(),
582 json!("web_search_2025_08_26")
583 );
584 }
585
586 #[test]
587 fn text_format_serializes_json_schema() {
588 let format = TextFormat::json_schema(
589 "math_response",
590 json!({"type": "object", "properties": {"answer": {"type": "number"}}}),
591 );
592 assert_eq!(
593 serde_json::to_value(format).unwrap(),
594 json!({
595 "type": "json_schema",
596 "name": "math_response",
597 "schema": {"type": "object", "properties": {"answer": {"type": "number"}}}
598 })
599 );
600 }
601
602 #[test]
603 fn tool_choice_serializes_mode_and_named() {
604 assert_eq!(
605 serde_json::to_value(ToolChoice::auto()).unwrap(),
606 json!("auto")
607 );
608 assert_eq!(
609 serde_json::to_value(ToolChoice::named("get_weather")).unwrap(),
610 json!({"type": "function", "name": "get_weather"})
611 );
612 }
613
614 #[test]
615 fn request_serializes_full_payload() {
616 let req = ResponsesRequestBuilder::default()
617 .client(client())
618 .model("deepseek-v4-flash")
619 .input("Hi")
620 .instructions("You are a helpful assistant.")
621 .reasoning(Reasoning::new(ReasoningEffort::Low))
622 .max_output_tokens(256_u32)
623 .temperature(0.7_f64)
624 .tool(Tool::function("get_weather", "Get the weather", None))
625 .build()
626 .unwrap();
627
628 let value = serde_json::to_value(&req).unwrap();
629 assert_eq!(value.get("model"), Some(&json!("deepseek-v4-flash")));
630 assert_eq!(value.get("input"), Some(&json!("Hi")));
631 assert_eq!(value.get("reasoning"), Some(&json!({"effort": "low"})));
632 assert_eq!(value.get("client"), None);
633 }
634
635 #[test]
636 fn builder_validation_rejects_invalid_values() {
637 let base = || {
638 ResponsesRequestBuilder::default()
639 .client(client())
640 .model("deepseek-v4-flash")
641 };
642
643 assert!(base().build().is_err(), "no input nor instructions");
644
645 assert!(
646 base().input("Hi").temperature(2.5_f64).build().is_err(),
647 "temperature out of range"
648 );
649 assert!(
650 base().input("Hi").top_p(1.5_f64).build().is_err(),
651 "top_p out of range"
652 );
653 assert!(
654 base().input("Hi").top_logprobs(21_u32).build().is_err(),
655 "top_logprobs out of range"
656 );
657 assert!(
658 base().input("Hi").user("not allowed!").build().is_err(),
659 "user charset"
660 );
661 assert!(
662 base().instructions("sys").build().is_ok(),
663 "instructions alone is valid"
664 );
665 }
666
667 #[test]
668 fn deserialize_unknown_enum_variants() {
669 let typ: InputItemType = serde_json::from_value(json!("file_search")).unwrap();
670 assert_eq!(typ, InputItemType::Unknown);
671
672 let role: InputRole = serde_json::from_value(json!("bot")).unwrap();
673 assert_eq!(role, InputRole::Unknown);
674 }
675}