Skip to main content

deepseek_sdk/responses/
request.rs

1use super::*;
2use derive_builder::Builder;
3
4/// Responses request body for the OpenAI Responses API format.
5#[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    /// ID of the model to use. The Responses API currently only supports `deepseek-v4-flash`.
17    pub model: String,
18
19    /// The input to the model. Either a plain string (treated as a single `user` message),
20    /// or a list of input items.
21    ///
22    /// At least one of `input` and `instructions` is required.
23    #[builder(default)]
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub input: Option<Input>,
26
27    /// A system-level instruction, inserted as the first system message of the model's context.
28    #[builder(default)]
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub instructions: Option<String>,
31
32    /// Configuration of the thinking mode.
33    #[builder(default)]
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub reasoning: Option<Reasoning>,
36
37    /// An upper bound for the number of tokens that can be generated in the response,
38    /// including both the visible output tokens and the reasoning tokens.
39    #[builder(default)]
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub max_output_tokens: Option<u32>,
42
43    /// If set to `true`, the response is streamed as semantic server-sent events.
44    /// The final event is `response.completed` / `response.incomplete` / `response.failed`
45    /// (there is no `data: [DONE]` message).
46    #[builder(default)]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub stream: Option<bool>,
49
50    /// Possible values: `<= 2`
51    ///
52    /// Default value: `1`
53    ///
54    /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output
55    /// more random, while lower values like 0.2 will make it more focused and deterministic.
56    /// Has no effect in thinking mode.
57    #[builder(default)]
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub temperature: Option<f64>,
60
61    /// Possible values: `<= 1`
62    ///
63    /// Default value: `1`
64    ///
65    /// An alternative to sampling with temperature, called nucleus sampling.
66    /// Has no effect in thinking mode.
67    #[builder(default)]
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub top_p: Option<f64>,
70
71    /// Configuration of the text output.
72    #[builder(default)]
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub text: Option<Text>,
75
76    /// A list of tools the model may call. Function names must be non-empty, at most 128 characters,
77    /// match `^[a-zA-Z0-9_-]+$`, and be unique across all tools.
78    /// Besides `function`, the built-in `web_search` tool is supported and executed on the server side.
79    #[builder(default, setter(each(name = "tool", into)))]
80    #[serde(skip_serializing_if = "Vec::is_empty")]
81    pub tools: Vec<Tool>,
82
83    /// Controls which (if any) tool is called by the model.
84    ///
85    /// `none` means the model will not call any tool and instead generates a message.
86    /// `auto` (default) means the model can pick between generating a message or calling one or more tools.
87    /// `required` means the model must call one or more tools.
88    ///
89    /// Specifying a particular tool via `{"type": "function", "name": "my_function"}` forces the model
90    /// to call that tool.
91    #[builder(default)]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub tool_choice: Option<ToolChoice>,
94
95    /// Possible values: `<= 20`
96    ///
97    /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token
98    /// position, each with an associated log probability.
99    #[builder(default)]
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub top_logprobs: Option<u32>,
102
103    /// A custom end-user identifier, with allowed character set `[a-zA-Z0-9\-_]` and a maximum length
104    /// of 512. Do not include user privacy information.
105    #[builder(default)]
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub user: Option<String>,
108}
109
110/// The input to the model. Either a plain string (treated as a single `user` message),
111/// or a list of input items.
112#[derive(Clone, Debug, PartialEq, Serialize)]
113#[serde(untagged)]
114pub enum Input {
115    /// A plain string, treated as a single `user` message.
116    TextInput(String),
117    /// A list of input items.
118    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/// A single input item of a Responses request.
140#[derive(Clone, Debug, PartialEq, Serialize)]
141pub struct InputItem {
142    /// The type of the input item. For `message` items, this field can be omitted if `role` is present.
143    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
144    pub typ: Option<InputItemType>,
145
146    /// For `message` items. The role of the message author. `developer` is treated as `system`.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub role: Option<InputRole>,
149
150    /// For `message` items, the message content, either a plain string or a list of content parts.
151    /// For `reasoning` items, a list of `reasoning_text` content parts.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub content: Option<InputContent>,
154
155    /// For `function_call` / `function_call_output` items. The ID pairing a function call with its output.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub call_id: Option<String>,
158
159    /// For `function_call` items. The name of the function to call.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub name: Option<String>,
162
163    /// For `function_call` items. The arguments to call the function with, in JSON format.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub arguments: Option<String>,
166
167    /// For `function_call_output` items. The output of the function call.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub output: Option<String>,
170}
171
172impl InputItem {
173    /// Build a `user` message input item from plain text.
174    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    /// Build an `assistant` message input item from plain text.
187    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    /// Build a `function_call` input item.
200    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    /// Build a `function_call_output` input item.
217    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/// The type of an input item.
231#[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    /// An unrecognized input item type is ignored by the server.
241    #[serde(other)]
242    Unknown,
243}
244
245/// The role of a message input item.
246#[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    /// Treated as `system` by the server.
254    Developer,
255    #[serde(other)]
256    Unknown,
257}
258
259/// Message content, either a plain string or a list of content parts.
260#[derive(Clone, Debug, PartialEq, Serialize)]
261#[serde(untagged)]
262pub enum InputContent {
263    /// A plain string message body.
264    Text(String),
265    /// A list of content parts.
266    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/// A content part of a message input item.
282#[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/// Configuration of the thinking mode.
290#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
291pub struct Reasoning {
292    /// Controls the thinking mode toggle and the thinking effort.
293    pub effort: ReasoningEffort,
294}
295
296impl Reasoning {
297    pub fn new(effort: ReasoningEffort) -> Self {
298        Reasoning { effort }
299    }
300}
301
302/// Thinking effort levels.
303#[non_exhaustive]
304#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
305#[serde(rename_all = "snake_case")]
306pub enum ReasoningEffort {
307    /// Disables thinking mode.
308    None,
309    Minimal,
310    Low,
311    Medium,
312    High,
313    /// Mapped to effort `high`.
314    #[serde(rename = "xhigh")]
315    XHigh,
316    /// Enables thinking mode with effort `max`.
317    #[serde(rename = "max")]
318    Max,
319}
320
321/// Configuration of the text output.
322#[derive(Clone, Debug, PartialEq, Serialize)]
323pub struct Text {
324    /// The output format.
325    pub format: TextFormat,
326}
327
328impl Text {
329    pub fn new(format: TextFormat) -> Self {
330        Text { format }
331    }
332}
333
334/// The output format of the response.
335#[derive(Clone, Debug, PartialEq, Serialize)]
336#[serde(tag = "type", rename_all = "snake_case")]
337pub enum TextFormat {
338    /// Plain text (default).
339    Text,
340    /// JSON mode.
341    JsonObject,
342    /// Structured output conforming to the given JSON Schema.
343    JsonSchema {
344        /// The name of the schema. Required when `type` is `json_schema`.
345        name: String,
346        /// The JSON Schema that the output must conform to.
347        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/// A tool the model may call.
369#[derive(Clone, Debug, PartialEq, Serialize)]
370pub struct Tool {
371    /// The type of the tool.
372    #[serde(rename = "type")]
373    pub typ: ToolType,
374
375    /// For `function` tools. The name of the function. Must be non-empty, at most 128 characters,
376    /// match `^[a-zA-Z0-9_-]+$`, and be unique across all tools.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub name: Option<String>,
379
380    /// For `function` tools. A description of what the function does.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub description: Option<String>,
383
384    /// The parameters the function accepts, described as a JSON Schema object.
385    ///
386    /// Omitting `parameters` defines a function with an empty parameter list.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub parameters: Option<serde_json::Value>,
389}
390
391impl Tool {
392    /// Build a `function` tool.
393    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    /// Build a server-side `web_search` tool.
407    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/// Tool type.
418#[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/// Tool choice configuration.
429#[non_exhaustive]
430#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
431#[serde(untagged)]
432pub enum ToolChoice {
433    /// Possible values: [`none`, `auto`, `required`]
434    Mode(ToolChoiceMode),
435    /// A specific tool, e.g. `{"type": "function", "name": "my_function"}`.
436    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/// Tool choice modes.
468#[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/// A named tool choice.
478#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
479pub struct NamedToolChoice {
480    /// Possible values: [`function`, `web_search`, `web_search_2025_08_26`]
481    #[serde(rename = "type")]
482    pub typ: ToolType,
483    /// The name of the function to call. Required when `type` is `function`.
484    #[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}