Skip to main content

ds_api/api/
request.rs

1//! ApiRequest builder module.
2//!
3//! Provides a safe, chainable request builder that wraps the internal
4//! `crate::raw::ChatCompletionRequest`.
5
6use crate::raw::{ChatCompletionRequest, Message, ResponseFormat, ResponseFormatType, Tool};
7
8/// A safe, chainable request builder that wraps `ChatCompletionRequest`.
9///
10/// Use [`with_model`][ApiRequest::with_model] to set an arbitrary model string,
11/// or the convenience constructors [`deepseek_chat`][ApiRequest::deepseek_chat]
12/// and [`deepseek_reasoner`][ApiRequest::deepseek_reasoner] for the standard
13/// DeepSeek models.
14#[derive(Debug)]
15pub struct ApiRequest {
16    raw: ChatCompletionRequest,
17}
18
19impl ApiRequest {
20    /// Start a new builder with default values.
21    pub fn builder() -> Self {
22        Self {
23            raw: ChatCompletionRequest::default(),
24        }
25    }
26
27    /// Set the model by string (builder-style).
28    ///
29    /// Accepts any model identifier — a named DeepSeek model or any
30    /// OpenAI-compatible model string:
31    ///
32    /// ```
33    /// use ds_api::ApiRequest;
34    ///
35    /// let req = ApiRequest::builder().with_model("deepseek-chat");
36    /// let req = ApiRequest::builder().with_model("gpt-4o");
37    /// ```
38    pub fn with_model(mut self, name: impl Into<String>) -> Self {
39        self.raw.model = crate::raw::Model::Custom(name.into());
40        self
41    }
42
43    /// Convenience constructor: deepseek-chat + messages
44    pub fn deepseek_chat(messages: Vec<Message>) -> Self {
45        let mut r = Self::builder();
46        r.raw.messages = messages;
47        r.raw.model = crate::raw::Model::DeepseekChat;
48        r
49    }
50
51    /// Convenience constructor: deepseek-reasoner + messages
52    pub fn deepseek_reasoner(messages: Vec<Message>) -> Self {
53        let mut r = Self::builder();
54        r.raw.messages = messages;
55        r.raw.model = crate::raw::Model::DeepseekReasoner;
56        r
57    }
58
59    /// Add a message to the request.
60    pub fn add_message(mut self, msg: Message) -> Self {
61        self.raw.messages.push(msg);
62        self
63    }
64
65    /// Replace messages.
66    pub fn messages(mut self, msgs: Vec<Message>) -> Self {
67        self.raw.messages = msgs;
68        self
69    }
70
71    /// Request response as JSON object.
72    pub fn json(mut self) -> Self {
73        self.raw.response_format = Some(ResponseFormat {
74            r#type: ResponseFormatType::JsonObject,
75        });
76        self
77    }
78
79    /// Request response as plain text.
80    pub fn text(mut self) -> Self {
81        self.raw.response_format = Some(ResponseFormat {
82            r#type: ResponseFormatType::Text,
83        });
84        self
85    }
86
87    /// Set temperature.
88    pub fn temperature(mut self, t: f32) -> Self {
89        self.raw.temperature = Some(t);
90        self
91    }
92
93    /// Set max tokens.
94    pub fn max_tokens(mut self, n: u32) -> Self {
95        self.raw.max_tokens = Some(n);
96        self
97    }
98
99    /// Add a raw tool definition (from `crate::raw::Tool`).
100    pub fn add_tool(mut self, tool: Tool) -> Self {
101        if let Some(ref mut v) = self.raw.tools {
102            v.push(tool);
103        } else {
104            self.raw.tools = Some(vec![tool]);
105        }
106        self
107    }
108
109    /// Set tool choice to Auto.
110    pub fn tool_choice_auto(mut self) -> Self {
111        use crate::raw::request::tool_choice::{ToolChoice, ToolChoiceType};
112        self.raw.tool_choice = Some(ToolChoice::String(ToolChoiceType::Auto));
113        self
114    }
115
116    /// Enable/disable streaming (stream: true).
117    pub fn stream(mut self, enabled: bool) -> Self {
118        self.raw.stream = Some(enabled);
119        self
120    }
121
122    /// Merge arbitrary top-level JSON into the request body.
123    ///
124    /// Pass a `serde_json::Map<String, serde_json::Value>` of key/value pairs which
125    /// will be flattened into the top-level request JSON via the raw request's
126    /// `extra_body` field.
127    pub fn extra_body(mut self, map: serde_json::Map<String, serde_json::Value>) -> Self {
128        self.raw.extra_body = Some(map);
129        self
130    }
131
132    /// Add a single extra top-level field to the request body (in-place).
133    ///
134    /// This method mutates the internal `ChatCompletionRequest`'s `extra_body`
135    /// map, creating it if necessary, and inserts the provided `key`/`value`
136    /// pair. Values in `extra_body` are flattened into the top-level request
137    /// JSON when serialised due to `#[serde(flatten)]`, so they appear as peers
138    /// to fields such as `messages` and `model`.
139    ///
140    /// Use this when you hold a mutable `ApiRequest` and want to add
141    /// provider-specific or experimental top-level fields without constructing a
142    /// full `Map` first.
143    ///
144    /// Example:
145    ///
146    /// ```rust
147    /// # use ds_api::ApiRequest;
148    /// # use serde_json::json;
149    /// let mut req = ApiRequest::builder();
150    /// req.add_extra_field("x_flag", json!(true));
151    /// ```
152    pub fn add_extra_field(&mut self, key: impl Into<String>, value: serde_json::Value) {
153        if let Some(ref mut m) = self.raw.extra_body {
154            m.insert(key.into(), value);
155        } else {
156            let mut m = serde_json::Map::new();
157            m.insert(key.into(), value);
158            self.raw.extra_body = Some(m);
159        }
160    }
161
162    /// Builder-style helper that consumes the `ApiRequest`, adds an extra field,
163    /// and returns the modified request for chaining.
164    ///
165    /// This is convenient for fluent construction:
166    ///
167    /// ```rust
168    /// # use ds_api::ApiRequest;
169    /// # use serde_json::json;
170    /// let req = ApiRequest::builder()
171    ///     .with_extra_field("provider_opt", json!("x"))
172    ///     .with_model("deepseek-chat");
173    /// ```
174    pub fn with_extra_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
175        self.add_extra_field(key, value);
176        self
177    }
178
179    /// Compatibility alias for the single-field helper (builder-style).
180    ///
181    /// Historically the builder exposed `extra_field(...)`. This method is kept
182    /// as an alias for compatibility but prefer `with_extra_field` or
183    /// `add_extra_field` for clearer intent.
184    pub fn extra_field(self, key: impl Into<String>, value: serde_json::Value) -> Self {
185        self.with_extra_field(key, value)
186    }
187
188    /// Build and return the internal raw request (crate-internal use).
189    pub(crate) fn into_raw(self) -> ChatCompletionRequest {
190        self.raw
191    }
192}