Skip to content

Commit bfc3741

Browse files
anchpopclaude
andauthored
Add shared HTTP client with connection pooling (#41)
* Add shared HTTP client with connection pooling Previously every request created a new reqwest::Client via Client::new(), which meant zero connection reuse. Now ChatClient, EmbeddingsClient, FilesClient, and BatchClient all share a pooled reqwest::Client configured with pool_max_idle_per_host=256 and pool_idle_timeout=300s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Bump version to 0.16.0 Breaking change: added http_client field to ChatClient, EmbeddingsClient, FilesClient, and BatchClient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b3e356a commit bfc3741

7 files changed

Lines changed: 50 additions & 29 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tysm"
3-
version = "0.15.0"
3+
version = "0.16.0"
44
edition = "2021"
55
description = "Batteries-included Rust OpenAI Client"
66
license = "MIT"

src/batch.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub struct BatchClient {
3535
pub model: String,
3636
/// The client to use for file operations.
3737
pub files_client: FilesClient,
38+
/// Shared HTTP client with connection pooling
39+
pub http_client: Client,
3840
}
3941

4042
impl From<&ChatClient> for BatchClient {
@@ -46,6 +48,7 @@ impl From<&ChatClient> for BatchClient {
4648
endpoint: "/v1/chat/completions".to_string(),
4749
model: client.model.clone(),
4850
files_client: FilesClient::from(client),
51+
http_client: client.http_client.clone(),
4952
}
5053
}
5154
}
@@ -434,9 +437,9 @@ impl BatchClient {
434437
input_file_id: impl AsRef<str>,
435438
metadata: HashMap<String, String>,
436439
) -> Result<Batch, CreateBatchError> {
437-
let client = Client::new();
438440
let url = remove_trailing_slash(self.batches_url());
439-
let response = client
441+
let response = self
442+
.http_client
440443
.post(url)
441444
.header("Authorization", format!("Bearer {}", self.api_key))
442445
.header("Content-Type", "application/json")
@@ -474,9 +477,9 @@ impl BatchClient {
474477

475478
/// Get the status of a batch.
476479
pub async fn get_batch_status(&self, batch_id: &str) -> Result<Batch, GetBatchStatusError> {
477-
let client = Client::new();
478480
let url = self.batches_url().join(batch_id).unwrap();
479-
let response = client
481+
let response = self
482+
.http_client
480483
.get(url)
481484
.header("Authorization", format!("Bearer {}", self.api_key))
482485
.header("Content-Type", "application/json")
@@ -570,8 +573,8 @@ impl BatchClient {
570573

571574
/// Cancel a batch.
572575
pub async fn cancel_batch(&self, batch_id: &str) -> Result<Batch, CancelBatchError> {
573-
let client = Client::new();
574-
let response = client
576+
let response = self
577+
.http_client
575578
.post(
576579
self.batches_url()
577580
.join(batch_id)
@@ -653,8 +656,8 @@ impl BatchClient {
653656
url.set_query(Some(&query_params.join("&")));
654657
}
655658

656-
let client = Client::new();
657-
let response = client
659+
let response = self
660+
.http_client
658661
.get(remove_trailing_slash(url))
659662
.header("Authorization", format!("Bearer {}", self.api_key))
660663
.header("Content-Type", "application/json")

src/chat_completions.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ pub struct ChatClient {
6767

6868
/// Semaphore to limit the maximum number of concurrent requests
6969
pub semaphore: Semaphore,
70+
71+
/// Shared HTTP client with connection pooling
72+
pub http_client: Client,
7073
}
7174

7275
/// The role of a message.
@@ -570,6 +573,7 @@ impl ChatClient {
570573
reasoning_effort: None,
571574
extra_body: None,
572575
semaphore: Semaphore::new(100),
576+
http_client: crate::utils::pooled_client(),
573577
}
574578
}
575579

@@ -1252,9 +1256,8 @@ impl ChatClient {
12521256
async fn chat_uncached(&self, chat_request: &ChatRequest) -> Result<String, ChatError> {
12531257
let _permit = self.semaphore.acquire().await.unwrap();
12541258

1255-
let reqwest_client = Client::new();
1256-
1257-
let response = reqwest_client
1259+
let response = self
1260+
.http_client
12581261
.post(self.chat_completions_url())
12591262
.header("Authorization", format!("Bearer {}", self.api_key.clone()))
12601263
.header("Content-Type", "application/json")

src/embeddings.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ pub struct EmbeddingsClient {
100100

101101
/// Semaphore to limit the maximum number of concurrent requests
102102
pub semaphore: Semaphore,
103+
104+
/// Shared HTTP client with connection pooling
105+
pub http_client: Client,
103106
}
104107

105108
/// Errors that can occur when interacting with the ChatGPT API.
@@ -164,6 +167,7 @@ impl EmbeddingsClient {
164167
backup_cache_directory: None,
165168
extra_body: None,
166169
semaphore: Semaphore::new(100),
170+
http_client: crate::utils::pooled_client(),
167171
}
168172
}
169173

@@ -478,9 +482,8 @@ impl EmbeddingsClient {
478482
) -> Result<String, EmbeddingsError> {
479483
let _permit = self.semaphore.acquire().await.unwrap();
480484

481-
let client = Client::new();
482-
483-
let response = client
485+
let response = self
486+
.http_client
484487
.post(self.embeddings_url())
485488
.header("Authorization", format!("Bearer {}", self.api_key))
486489
.header("Content-Type", "application/json")

src/files.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ use crate::{
1414
};
1515

1616
/// A client for interacting with the OpenAI Files API.
17-
#[derive(Debug)]
1817
pub struct FilesClient {
1918
/// The API key to use for the OpenAI API.
2019
pub api_key: String,
2120
/// The base URL of the OpenAI API.
2221
pub base_url: url::Url,
2322
/// The path to the Files API.
2423
pub files_path: String,
24+
/// Shared HTTP client with connection pooling
25+
pub http_client: Client,
2526
}
2627

2728
impl From<&crate::chat_completions::ChatClient> for FilesClient {
@@ -30,6 +31,7 @@ impl From<&crate::chat_completions::ChatClient> for FilesClient {
3031
api_key: client.api_key.clone(),
3132
base_url: client.base_url.clone(),
3233
files_path: "files/".to_string(),
34+
http_client: client.http_client.clone(),
3335
}
3436
}
3537
}
@@ -156,6 +158,7 @@ impl FilesClient {
156158
api_key: api_key.into(),
157159
base_url: url::Url::parse("https://api.openai.com/v1/").unwrap(),
158160
files_path: "files/".to_string(),
161+
http_client: crate::utils::pooled_client(),
159162
}
160163
}
161164

@@ -206,9 +209,9 @@ impl FilesClient {
206209
.text("purpose", format!("{:?}", purpose).to_lowercase())
207210
.part("file", file_part);
208211

209-
let client = Client::new();
210212
let url = remove_trailing_slash(self.files_url());
211-
let response = client
213+
let response = self
214+
.http_client
212215
.post(url.clone())
213216
.header("Authorization", format!("Bearer {}", self.api_key))
214217
.multipart(form)
@@ -254,9 +257,9 @@ impl FilesClient {
254257
.text("purpose", format!("{:?}", purpose).to_lowercase())
255258
.part("file", file_part);
256259

257-
let client = Client::new();
258260
let url = remove_trailing_slash(self.files_url());
259-
let response = client
261+
let response = self
262+
.http_client
260263
.post(url.clone())
261264
.header("Authorization", format!("Bearer {}", self.api_key))
262265
.multipart(form)
@@ -292,8 +295,8 @@ impl FilesClient {
292295
/// # });
293296
/// ```
294297
pub async fn list_files(&self) -> Result<FileList, FilesError> {
295-
let client = Client::new();
296-
let response = client
298+
let response = self
299+
.http_client
297300
.get(self.files_url())
298301
.header("Authorization", format!("Bearer {}", self.api_key))
299302
.send()
@@ -315,8 +318,8 @@ impl FilesClient {
315318
/// # });
316319
/// ```
317320
pub async fn retrieve_file(&self, file_id: &str) -> Result<FileObject, FilesError> {
318-
let client = Client::new();
319-
let response = client
321+
let response = self
322+
.http_client
320323
.get(self.files_url().join(file_id).unwrap())
321324
.header("Authorization", format!("Bearer {}", self.api_key))
322325
.send()
@@ -338,8 +341,8 @@ impl FilesClient {
338341
/// # });
339342
/// ```
340343
pub async fn delete_file(&self, file_id: &str) -> Result<DeletedFile, FilesError> {
341-
let client = Client::new();
342-
let response = client
344+
let response = self
345+
.http_client
343346
.delete(self.files_url().join(file_id).unwrap())
344347
.header("Authorization", format!("Bearer {}", self.api_key))
345348
.send()
@@ -361,12 +364,12 @@ impl FilesClient {
361364
/// # });
362365
/// ```
363366
pub async fn download_file(&self, file_id: &str) -> Result<String, FilesError> {
364-
let client = Client::new();
365367
let url = self
366368
.files_url()
367369
.join(&format!("{file_id}/content"))
368370
.unwrap();
369-
let response = client
371+
let response = self
372+
.http_client
370373
.get(url)
371374
.header("Authorization", format!("Bearer {}", self.api_key))
372375
.send()

src/utils.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ pub(crate) fn remove_trailing_slash(url: url::Url) -> url::Url {
2424
url.set_path(path);
2525
url
2626
}
27+
28+
/// Create a shared reqwest::Client with connection pooling configured for high concurrency.
29+
pub(crate) fn pooled_client() -> reqwest::Client {
30+
reqwest::Client::builder()
31+
.pool_max_idle_per_host(256)
32+
.pool_idle_timeout(std::time::Duration::from_secs(300))
33+
.build()
34+
.expect("Failed to build HTTP client")
35+
}

0 commit comments

Comments
 (0)