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}