Skip to main content

openai_oxide/
request_options.rs

1// Per-request options for customizing individual API calls.
2
3use std::time::Duration;
4
5use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
6
7/// Options that customize individual API requests.
8///
9/// Use with [`OpenAI::with_options()`](crate::OpenAI::with_options) to create
10/// a client clone that applies these options to every request:
11///
12/// ```ignore
13/// use openai_oxide::RequestOptions;
14///
15/// let custom = client.with_options(
16///     RequestOptions::new()
17///         .header("X-Custom", "value")
18///         .timeout(Duration::from_secs(30))
19/// );
20/// let response = custom.chat().completions().create(request).await?;
21/// ```
22#[derive(Debug, Clone, Default)]
23pub struct RequestOptions {
24    /// Extra headers to include in the request.
25    pub headers: Option<HeaderMap>,
26
27    /// Extra query parameters to append to the URL.
28    pub query: Option<Vec<(String, String)>>,
29
30    /// Extra JSON fields to merge into the request body (JSON requests only).
31    pub extra_body: Option<serde_json::Value>,
32
33    /// Override the request timeout.
34    pub timeout: Option<Duration>,
35}
36
37impl RequestOptions {
38    /// Create empty request options.
39    #[must_use]
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Add a single header.
45    #[must_use]
46    pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
47        let map = self.headers.get_or_insert_with(HeaderMap::new);
48        if let (Ok(n), Ok(v)) = (
49            name.as_ref().parse::<HeaderName>(),
50            value.as_ref().parse::<HeaderValue>(),
51        ) {
52            map.insert(n, v);
53        }
54        self
55    }
56
57    /// Set multiple headers at once (replaces any previously set headers).
58    #[must_use]
59    pub fn headers(mut self, headers: HeaderMap) -> Self {
60        self.headers = Some(headers);
61        self
62    }
63
64    /// Add a single query parameter.
65    #[must_use]
66    pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
67        self.query
68            .get_or_insert_with(Vec::new)
69            .push((key.into(), value.into()));
70        self
71    }
72
73    /// Set all query parameters (replaces any previously set).
74    #[must_use]
75    pub fn query(mut self, params: Vec<(String, String)>) -> Self {
76        self.query = Some(params);
77        self
78    }
79
80    /// Set extra JSON fields to merge into the request body.
81    #[must_use]
82    pub fn extra_body(mut self, value: serde_json::Value) -> Self {
83        self.extra_body = Some(value);
84        self
85    }
86
87    /// Override the request timeout.
88    #[must_use]
89    pub fn timeout(mut self, duration: Duration) -> Self {
90        self.timeout = Some(duration);
91        self
92    }
93
94    /// Merge two options. Values from `other` take precedence on conflict.
95    #[must_use]
96    pub fn merge(&self, other: &RequestOptions) -> RequestOptions {
97        RequestOptions {
98            headers: merge_headers(&self.headers, &other.headers),
99            query: merge_query(&self.query, &other.query),
100            extra_body: merge_json(&self.extra_body, &other.extra_body),
101            timeout: other.timeout.or(self.timeout),
102        }
103    }
104
105    /// Returns true if no options are set.
106    pub fn is_empty(&self) -> bool {
107        self.headers.is_none()
108            && self.query.is_none()
109            && self.extra_body.is_none()
110            && self.timeout.is_none()
111    }
112}
113
114/// Merge two optional header maps. `b` values win on key collision.
115fn merge_headers(a: &Option<HeaderMap>, b: &Option<HeaderMap>) -> Option<HeaderMap> {
116    match (a, b) {
117        (None, None) => None,
118        (Some(a), None) => Some(a.clone()),
119        (None, Some(b)) => Some(b.clone()),
120        (Some(a), Some(b)) => {
121            let mut merged = a.clone();
122            for (key, value) in b.iter() {
123                merged.insert(key.clone(), value.clone());
124            }
125            Some(merged)
126        }
127    }
128}
129
130/// Merge two optional query param vecs. Both are appended; `b` comes after `a`.
131fn merge_query(
132    a: &Option<Vec<(String, String)>>,
133    b: &Option<Vec<(String, String)>>,
134) -> Option<Vec<(String, String)>> {
135    match (a, b) {
136        (None, None) => None,
137        (Some(a), None) => Some(a.clone()),
138        (None, Some(b)) => Some(b.clone()),
139        (Some(a), Some(b)) => {
140            let mut merged = a.clone();
141            merged.extend(b.iter().cloned());
142            Some(merged)
143        }
144    }
145}
146
147/// Deep-merge two optional JSON values. `b` fields win on conflict.
148fn merge_json(
149    a: &Option<serde_json::Value>,
150    b: &Option<serde_json::Value>,
151) -> Option<serde_json::Value> {
152    match (a, b) {
153        (None, None) => None,
154        (Some(a), None) => Some(a.clone()),
155        (None, Some(b)) => Some(b.clone()),
156        (Some(a), Some(b)) => Some(deep_merge_value(a.clone(), b.clone())),
157    }
158}
159
160/// Recursively merge two JSON values. Object fields from `b` override `a`.
161fn deep_merge_value(a: serde_json::Value, b: serde_json::Value) -> serde_json::Value {
162    use serde_json::Value;
163    match (a, b) {
164        (Value::Object(mut a_map), Value::Object(b_map)) => {
165            for (k, v) in b_map {
166                let merged = match a_map.remove(&k) {
167                    Some(existing) => deep_merge_value(existing, v),
168                    None => v,
169                };
170                a_map.insert(k, merged);
171            }
172            Value::Object(a_map)
173        }
174        // Non-object: b wins
175        (_, b) => b,
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_new_is_empty() {
185        let opts = RequestOptions::new();
186        assert!(opts.is_empty());
187    }
188
189    #[test]
190    fn test_header_builder() {
191        let opts = RequestOptions::new()
192            .header("X-Custom", "value1")
193            .header("X-Other", "value2");
194        let headers = opts.headers.unwrap();
195        assert_eq!(headers.get("X-Custom").unwrap(), "value1");
196        assert_eq!(headers.get("X-Other").unwrap(), "value2");
197    }
198
199    #[test]
200    fn test_query_param_builder() {
201        let opts = RequestOptions::new()
202            .query_param("foo", "bar")
203            .query_param("baz", "qux");
204        let query = opts.query.unwrap();
205        assert_eq!(query.len(), 2);
206        assert_eq!(query[0], ("foo".to_string(), "bar".to_string()));
207    }
208
209    #[test]
210    fn test_timeout_builder() {
211        let opts = RequestOptions::new().timeout(Duration::from_secs(30));
212        assert_eq!(opts.timeout, Some(Duration::from_secs(30)));
213    }
214
215    #[test]
216    fn test_extra_body_builder() {
217        let opts = RequestOptions::new().extra_body(serde_json::json!({"key": "val"}));
218        assert_eq!(opts.extra_body.unwrap(), serde_json::json!({"key": "val"}));
219    }
220
221    #[test]
222    fn test_merge_empty() {
223        let a = RequestOptions::new();
224        let b = RequestOptions::new();
225        let merged = a.merge(&b);
226        assert!(merged.is_empty());
227    }
228
229    #[test]
230    fn test_merge_headers_b_wins() {
231        let a = RequestOptions::new().header("X-A", "1").header("X-B", "2");
232        let b = RequestOptions::new().header("X-A", "overridden");
233        let merged = a.merge(&b);
234        let headers = merged.headers.unwrap();
235        assert_eq!(headers.get("X-A").unwrap(), "overridden");
236        assert_eq!(headers.get("X-B").unwrap(), "2");
237    }
238
239    #[test]
240    fn test_merge_query_appends() {
241        let a = RequestOptions::new().query_param("a", "1");
242        let b = RequestOptions::new().query_param("b", "2");
243        let merged = a.merge(&b);
244        let query = merged.query.unwrap();
245        assert_eq!(query.len(), 2);
246    }
247
248    #[test]
249    fn test_merge_timeout_b_wins() {
250        let a = RequestOptions::new().timeout(Duration::from_secs(10));
251        let b = RequestOptions::new().timeout(Duration::from_secs(30));
252        let merged = a.merge(&b);
253        assert_eq!(merged.timeout, Some(Duration::from_secs(30)));
254    }
255
256    #[test]
257    fn test_merge_timeout_a_kept_when_b_none() {
258        let a = RequestOptions::new().timeout(Duration::from_secs(10));
259        let b = RequestOptions::new();
260        let merged = a.merge(&b);
261        assert_eq!(merged.timeout, Some(Duration::from_secs(10)));
262    }
263
264    #[test]
265    fn test_deep_merge_json() {
266        let a = serde_json::json!({"x": 1, "nested": {"a": 1, "b": 2}});
267        let b = serde_json::json!({"y": 2, "nested": {"b": 99, "c": 3}});
268        let merged = deep_merge_value(a, b);
269        assert_eq!(merged["x"], 1);
270        assert_eq!(merged["y"], 2);
271        assert_eq!(merged["nested"]["a"], 1);
272        assert_eq!(merged["nested"]["b"], 99);
273        assert_eq!(merged["nested"]["c"], 3);
274    }
275}