Skip to main content

openai_rust2/
lib.rs

1pub extern crate futures_util;
2use anyhow::{anyhow, Result};
3use lazy_static::lazy_static;
4use std::time::Duration;
5
6lazy_static! {
7    static ref DEFAULT_BASE_URL: reqwest::Url =
8        reqwest::Url::parse("https://api.openai.com/v1/models").unwrap();
9
10    /// Shared HTTP client with optimized connection pooling for high-throughput LLM workloads.
11    ///
12    /// Configuration rationale:
13    /// - pool_max_idle_per_host: 100 (handle burst traffic without connection churn)
14    /// - pool_idle_timeout: 90s (keep connections warm between requests)
15    /// - tcp_keepalive: 60s (detect dead connections proactively)
16    /// - connect_timeout: 10s (fail fast on connection issues)
17    /// - timeout: 300s (generous for large model responses)
18    /// - tcp_nodelay: true (reduce latency for small requests)
19    static ref SHARED_HTTP_CLIENT: reqwest::Client = {
20        reqwest::ClientBuilder::new()
21            .pool_idle_timeout(Some(Duration::from_secs(90)))
22            .pool_max_idle_per_host(100)
23            .tcp_keepalive(Some(Duration::from_secs(60)))
24            .timeout(Duration::from_secs(300))
25            .connect_timeout(Duration::from_secs(10))
26            .tcp_nodelay(true)
27            .build()
28            .expect("Failed to build shared HTTP client")
29    };
30}
31
32pub struct Client {
33    req_client: reqwest::Client,
34    key: String,
35    base_url: reqwest::Url,
36}
37
38pub mod chat;
39pub mod completions;
40pub mod edits;
41pub mod embeddings;
42pub mod images;
43pub mod models;
44
45impl Client {
46    /// Create a new client with the shared optimized HTTP client.
47    /// Uses connection pooling with keep-alive for high-throughput workloads.
48    pub fn new(api_key: &str) -> Client {
49        Client {
50            req_client: SHARED_HTTP_CLIENT.clone(),
51            key: api_key.to_owned(),
52            base_url: DEFAULT_BASE_URL.clone(),
53        }
54    }
55
56    /// Create a new client with a custom reqwest::Client.
57    /// Use this when you need custom TLS, proxy, or connection pool settings.
58    pub fn new_with_client(api_key: &str, req_client: reqwest::Client) -> Client {
59        Client {
60            req_client,
61            key: api_key.to_owned(),
62            base_url: DEFAULT_BASE_URL.clone(),
63        }
64    }
65
66    /// Create a new client with the shared optimized HTTP client and custom base URL.
67    pub fn new_with_base_url(api_key: &str, base_url: &str) -> Client {
68        let base_url = reqwest::Url::parse(base_url).unwrap();
69        Client {
70            req_client: SHARED_HTTP_CLIENT.clone(),
71            key: api_key.to_owned(),
72            base_url,
73        }
74    }
75
76    /// Create a new client with a custom reqwest::Client and custom base URL.
77    pub fn new_with_client_and_base_url(
78        api_key: &str,
79        req_client: reqwest::Client,
80        base_url: &str,
81    ) -> Client {
82        Client {
83            req_client,
84            key: api_key.to_owned(),
85            base_url: reqwest::Url::parse(base_url).unwrap(),
86        }
87    }
88
89    /// Get a reference to the shared HTTP client for advanced usage.
90    pub fn shared_client() -> &'static reqwest::Client {
91        &SHARED_HTTP_CLIENT
92    }
93
94    /// Helper to send a POST request with JSON body and parse the response.
95    /// Returns the raw text for non-200 responses, or parses JSON for 200 responses.
96    async fn send_json<T: serde::de::DeserializeOwned>(
97        &self,
98        path: &str,
99        body: &impl serde::Serialize,
100        error_context: &str,
101    ) -> Result<T, anyhow::Error> {
102        let mut url = self.base_url.clone();
103        url.set_path(path);
104
105        let res = self
106            .req_client
107            .post(url)
108            .bearer_auth(&self.key)
109            .json(body)
110            .send()
111            .await?;
112
113        let status = res.status();
114        let text = res.text().await?;
115
116        if status == 200 {
117            serde_json::from_str(&text)
118                .map_err(|e| anyhow!("{} failed to parse: {}. Raw: {}", error_context, e, text))
119        } else {
120            Err(anyhow!(
121                "{} API error ({}): {}",
122                error_context,
123                status,
124                text
125            ))
126        }
127    }
128
129    /// Helper for GET requests
130    async fn send_get<T: serde::de::DeserializeOwned>(
131        &self,
132        path: &str,
133        error_context: &str,
134    ) -> Result<T, anyhow::Error> {
135        let mut url = self.base_url.clone();
136        url.set_path(path);
137
138        let res = self
139            .req_client
140            .get(url)
141            .bearer_auth(&self.key)
142            .send()
143            .await?;
144
145        let status = res.status();
146        let text = res.text().await?;
147
148        if status == 200 {
149            serde_json::from_str(&text)
150                .map_err(|e| anyhow!("{} failed to parse: {}. Raw: {}", error_context, e, text))
151        } else {
152            Err(anyhow!(
153                "{} API error ({}): {}",
154                error_context,
155                status,
156                text
157            ))
158        }
159    }
160
161    pub async fn list_models(
162        &self,
163        opt_url_path: Option<String>,
164    ) -> Result<Vec<models::Model>, anyhow::Error> {
165        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/models"));
166        #[derive(serde::Deserialize)]
167        struct ListModelsResponse {
168            data: Vec<models::Model>,
169        }
170        let response: ListModelsResponse = self.send_get(&path, "list_models").await?;
171        Ok(response.data)
172    }
173
174    pub async fn create_chat(
175        &self,
176        args: chat::ChatArguments,
177        opt_url_path: Option<String>,
178    ) -> Result<chat::ChatCompletion, anyhow::Error> {
179        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/chat/completions"));
180        self.send_json(&path, &args, "create_chat").await
181    }
182
183    pub async fn create_chat_stream(
184        &self,
185        args: chat::ChatArguments,
186        opt_url_path: Option<String>,
187    ) -> Result<chat::stream::ChatCompletionChunkStream> {
188        let mut url = self.base_url.clone();
189        url.set_path(&opt_url_path.unwrap_or_else(|| String::from("/v1/chat/completions")));
190
191        let mut args = args;
192        args.stream = Some(true);
193
194        let res = self
195            .req_client
196            .post(url)
197            .bearer_auth(&self.key)
198            .json(&args)
199            .send()
200            .await?;
201
202        if res.status() == 200 {
203            Ok(chat::stream::ChatCompletionChunkStream::new(Box::pin(
204                res.bytes_stream(),
205            )))
206        } else {
207            Err(anyhow!(res.text().await?))
208        }
209    }
210
211    pub async fn create_completion(
212        &self,
213        args: completions::CompletionArguments,
214        opt_url_path: Option<String>,
215    ) -> Result<completions::CompletionResponse> {
216        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/completions"));
217        self.send_json(&path, &args, "create_completion").await
218    }
219
220    pub async fn create_embeddings(
221        &self,
222        args: embeddings::EmbeddingsArguments,
223        opt_url_path: Option<String>,
224    ) -> Result<embeddings::EmbeddingsResponse> {
225        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/embeddings"));
226        self.send_json(&path, &args, "create_embeddings").await
227    }
228
229    pub async fn create_image_old(
230        &self,
231        args: images::ImageArguments,
232        opt_url_path: Option<String>,
233    ) -> Result<Vec<String>> {
234        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/images/generations"));
235        let response: images::ImageResponse =
236            self.send_json(&path, &args, "create_image_old").await?;
237        Ok(response
238            .data
239            .iter()
240            .map(|o| match o {
241                images::ImageObject::Url(s) => s.to_string(),
242                images::ImageObject::Base64JSON(s) => s.to_string(),
243            })
244            .collect())
245    }
246
247    pub async fn create_image(
248        &self,
249        args: images::ImageArguments,
250        opt_url_path: Option<String>,
251    ) -> Result<Vec<String>> {
252        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/images/generations"));
253        let image_args = images::ImageArguments {
254            prompt: args.prompt,
255            model: Some("gpt-image-1".to_string()),
256            n: Some(1),
257            size: Some("1024x1024".to_string()),
258            quality: Some("auto".to_string()),
259            user: None,
260        };
261        let response: images::ImageResponse =
262            self.send_json(&path, &image_args, "create_image").await?;
263        Ok(response
264            .data
265            .iter()
266            .map(|o| match o {
267                images::ImageObject::Url(s) => s.to_string(),
268                images::ImageObject::Base64JSON(s) => s.to_string(),
269            })
270            .collect())
271    }
272
273    /// Create a response using xAI's Responses API with agentic tool calling.
274    ///
275    /// This method calls the `/v1/responses` endpoint which supports server-side
276    /// tools like web_search, x_search, code_execution, and more.
277    ///
278    /// # Arguments
279    /// * `args` - The ResponsesArguments containing model, input messages, and tools
280    /// * `opt_url_path` - Optional URL path override (defaults to `/v1/responses`)
281    ///
282    /// # Example
283    /// ```rust,no_run
284    /// use openai_rust2::chat::{ResponsesArguments, ResponsesMessage, GrokTool};
285    /// use openai_rust2::Client;
286    ///
287    /// async fn example() -> anyhow::Result<()> {
288    ///     let client = Client::new_with_base_url("your-api-key", "https://api.x.ai/v1");
289    ///     let args = ResponsesArguments::new(
290    ///         "grok-4-1-fast-reasoning",
291    ///         vec![ResponsesMessage {
292    ///             role: "user".to_string(),
293    ///             content: "What is the current Bitcoin price?".to_string(),
294    ///         }],
295    ///     ).with_tools(vec![GrokTool::web_search()]);
296    ///
297    ///     let response = client.create_responses(args, None).await?;
298    ///     println!("{}", response.get_text_content());
299    ///     Ok(())
300    /// }
301    /// ```
302    pub async fn create_responses(
303        &self,
304        args: chat::ResponsesArguments,
305        opt_url_path: Option<String>,
306    ) -> Result<chat::ResponsesCompletion, anyhow::Error> {
307        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/responses"));
308        self.send_json(&path, &args, "create_responses").await
309    }
310
311    /// Create a response using OpenAI's Responses API with agentic tool calling.
312    ///
313    /// This method calls the `/v1/responses` endpoint which supports server-side
314    /// tools like web_search, file_search, and code_interpreter.
315    ///
316    /// Supported models: gpt-5, gpt-4o, and other models with tool support.
317    ///
318    /// # Arguments
319    /// * `args` - The OpenAIResponsesArguments containing model, input messages, and tools
320    /// * `opt_url_path` - Optional URL path override (defaults to `/v1/responses`)
321    ///
322    /// # Example
323    /// ```rust,no_run
324    /// use openai_rust2::chat::{OpenAIResponsesArguments, ResponsesMessage, OpenAITool};
325    /// use openai_rust2::Client;
326    ///
327    /// async fn example() -> anyhow::Result<()> {
328    ///     let client = Client::new("your-openai-api-key");
329    ///     let args = OpenAIResponsesArguments::new(
330    ///         "gpt-5",
331    ///         vec![ResponsesMessage {
332    ///             role: "user".to_string(),
333    ///             content: "What are the latest developments in AI?".to_string(),
334    ///         }],
335    ///     ).with_tools(vec![OpenAITool::web_search()]);
336    ///
337    ///     let response = client.create_openai_responses(args, None).await?;
338    ///     println!("{}", response.get_text_content());
339    ///     Ok(())
340    /// }
341    /// ```
342    pub async fn create_openai_responses(
343        &self,
344        args: chat::OpenAIResponsesArguments,
345        opt_url_path: Option<String>,
346    ) -> Result<chat::ResponsesCompletion, anyhow::Error> {
347        let path = opt_url_path.unwrap_or_else(|| String::from("/v1/responses"));
348        self.send_json(&path, &args, "create_openai_responses")
349            .await
350    }
351}