1use std::sync::Arc;
31use std::time::Duration;
32
33use secrecy::SecretString;
34
35use crate::auth::CredentialProvider;
36#[cfg(any(feature = "native-http", feature = "wasm-http"))]
37use crate::error::Result;
38use crate::http::transport::TransportConfig;
39#[cfg(feature = "tower")]
40use crate::tower::{BudgetConfig, CacheConfig, CacheStore, InFlightLimitConfig, LlmHook, RateLimitConfig};
41
42pub struct NoApiKey;
44pub struct WithApiKey;
46pub struct NoProvider;
48pub struct WithProvider;
50
51#[must_use = "call .build() to construct the client"]
86pub struct ClientBuilder<K = NoApiKey, P = NoProvider> {
87 api_key: SecretString,
88 provider_hint: String,
89 base_url: Option<String>,
90 timeout: Duration,
91 connect_timeout: Duration,
92 max_retries: u32,
93 max_response_bytes: Option<usize>,
94 transport: TransportConfig,
95 load_env: bool,
96 credential_provider: Option<Arc<dyn CredentialProvider>>,
97 #[cfg(feature = "tower")]
98 cache_config: Option<CacheConfig>,
99 #[cfg(feature = "tower")]
100 cache_store: Option<Arc<dyn CacheStore>>,
101 #[cfg(feature = "tower")]
102 budget_config: Option<BudgetConfig>,
103 #[cfg(feature = "tower")]
104 hooks: Vec<Arc<dyn LlmHook>>,
105 #[cfg(feature = "tower")]
106 cooldown_duration: Option<Duration>,
107 #[cfg(feature = "tower")]
108 rate_limit_config: Option<RateLimitConfig>,
109 #[cfg(feature = "tower")]
110 in_flight_limit_config: Option<InFlightLimitConfig>,
111 #[cfg(feature = "tower")]
112 health_check_interval: Option<Duration>,
113 #[cfg(feature = "tower")]
114 enable_cost_tracking: bool,
115 #[cfg(feature = "tower")]
116 enable_tracing: bool,
117 _key_state: std::marker::PhantomData<K>,
118 _provider_state: std::marker::PhantomData<P>,
119}
120
121impl ClientBuilder<NoApiKey, NoProvider> {
122 pub fn new() -> Self {
127 Self {
128 api_key: SecretString::from(String::new()),
129 provider_hint: String::new(),
130 base_url: None,
131 timeout: Duration::from_secs(60),
132 connect_timeout: Duration::from_secs(10),
133 max_retries: 3,
134 max_response_bytes: None,
135 transport: TransportConfig::default(),
136 load_env: false,
137 credential_provider: None,
138 #[cfg(feature = "tower")]
139 cache_config: None,
140 #[cfg(feature = "tower")]
141 cache_store: None,
142 #[cfg(feature = "tower")]
143 budget_config: None,
144 #[cfg(feature = "tower")]
145 hooks: Vec::new(),
146 #[cfg(feature = "tower")]
147 cooldown_duration: None,
148 #[cfg(feature = "tower")]
149 rate_limit_config: None,
150 #[cfg(feature = "tower")]
151 in_flight_limit_config: None,
152 #[cfg(feature = "tower")]
153 health_check_interval: None,
154 #[cfg(feature = "tower")]
155 enable_cost_tracking: false,
156 #[cfg(feature = "tower")]
157 enable_tracing: false,
158 _key_state: std::marker::PhantomData,
159 _provider_state: std::marker::PhantomData,
160 }
161 }
162}
163
164impl Default for ClientBuilder<NoApiKey, NoProvider> {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl<K, P> ClientBuilder<K, P> {
171 pub fn api_key(self, key: impl Into<String>) -> ClientBuilder<WithApiKey, P> {
179 ClientBuilder {
180 api_key: SecretString::from(key.into()),
181 provider_hint: self.provider_hint,
182 base_url: self.base_url,
183 timeout: self.timeout,
184 connect_timeout: self.connect_timeout,
185 max_retries: self.max_retries,
186 max_response_bytes: self.max_response_bytes,
187 transport: self.transport,
188 load_env: self.load_env,
189 credential_provider: self.credential_provider,
190 #[cfg(feature = "tower")]
191 cache_config: self.cache_config,
192 #[cfg(feature = "tower")]
193 cache_store: self.cache_store,
194 #[cfg(feature = "tower")]
195 budget_config: self.budget_config,
196 #[cfg(feature = "tower")]
197 hooks: self.hooks,
198 #[cfg(feature = "tower")]
199 cooldown_duration: self.cooldown_duration,
200 #[cfg(feature = "tower")]
201 rate_limit_config: self.rate_limit_config,
202 #[cfg(feature = "tower")]
203 in_flight_limit_config: self.in_flight_limit_config,
204 #[cfg(feature = "tower")]
205 health_check_interval: self.health_check_interval,
206 #[cfg(feature = "tower")]
207 enable_cost_tracking: self.enable_cost_tracking,
208 #[cfg(feature = "tower")]
209 enable_tracing: self.enable_tracing,
210 _key_state: std::marker::PhantomData,
211 _provider_state: self._provider_state,
212 }
213 }
214
215 pub fn provider(self, provider_name: impl Into<String>) -> ClientBuilder<K, WithProvider> {
225 ClientBuilder {
226 api_key: self.api_key,
227 provider_hint: provider_name.into(),
228 base_url: self.base_url,
229 timeout: self.timeout,
230 connect_timeout: self.connect_timeout,
231 max_retries: self.max_retries,
232 max_response_bytes: self.max_response_bytes,
233 transport: self.transport,
234 load_env: self.load_env,
235 credential_provider: self.credential_provider,
236 #[cfg(feature = "tower")]
237 cache_config: self.cache_config,
238 #[cfg(feature = "tower")]
239 cache_store: self.cache_store,
240 #[cfg(feature = "tower")]
241 budget_config: self.budget_config,
242 #[cfg(feature = "tower")]
243 hooks: self.hooks,
244 #[cfg(feature = "tower")]
245 cooldown_duration: self.cooldown_duration,
246 #[cfg(feature = "tower")]
247 rate_limit_config: self.rate_limit_config,
248 #[cfg(feature = "tower")]
249 in_flight_limit_config: self.in_flight_limit_config,
250 #[cfg(feature = "tower")]
251 health_check_interval: self.health_check_interval,
252 #[cfg(feature = "tower")]
253 enable_cost_tracking: self.enable_cost_tracking,
254 #[cfg(feature = "tower")]
255 enable_tracing: self.enable_tracing,
256 _key_state: self._key_state,
257 _provider_state: std::marker::PhantomData,
258 }
259 }
260
261 pub fn base_url(mut self, url: impl Into<String>) -> Self {
266 let url = url.into();
267 self.base_url = Some(url.trim_end_matches('/').to_string());
268 self
269 }
270
271 pub fn timeout(mut self, timeout: Duration) -> Self {
273 self.timeout = timeout;
274 self
275 }
276
277 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
283 self.connect_timeout = timeout;
284 self
285 }
286
287 pub fn max_retries(mut self, retries: u32) -> Self {
289 self.max_retries = retries;
290 self
291 }
292
293 #[cfg(all(feature = "native-http", not(target_arch = "wasm32")))]
298 pub fn max_response_bytes(mut self, limit: usize) -> Result<Self> {
299 super::config::validate_response_limit(Some(limit))?;
300 self.max_response_bytes = Some(limit);
301 Ok(self)
302 }
303
304 pub fn transport(mut self, config: TransportConfig) -> Self {
306 self.transport = config;
307 self
308 }
309
310 pub fn load_env(mut self, enabled: bool) -> Self {
319 self.load_env = enabled;
320 self
321 }
322
323 pub fn credential_provider(mut self, provider: Arc<dyn CredentialProvider>) -> Self {
328 self.credential_provider = Some(provider);
329 self
330 }
331
332 #[cfg(feature = "tower")]
334 pub fn cache(mut self, config: CacheConfig) -> Self {
335 self.cache_config = Some(config);
336 self
337 }
338
339 #[cfg(feature = "tower")]
341 pub fn cache_store(mut self, store: Arc<dyn CacheStore>) -> Self {
342 self.cache_store = Some(store);
343 self
344 }
345
346 #[cfg(feature = "tower")]
348 pub fn budget(mut self, config: BudgetConfig) -> Self {
349 self.budget_config = Some(config);
350 self
351 }
352
353 #[cfg(feature = "tower")]
355 pub fn hook(mut self, hook: Arc<dyn LlmHook>) -> Self {
356 self.hooks.push(hook);
357 self
358 }
359
360 #[cfg(feature = "tower")]
362 pub fn hooks(mut self, hooks: Vec<Arc<dyn LlmHook>>) -> Self {
363 self.hooks = hooks;
364 self
365 }
366
367 #[cfg(feature = "tower")]
369 pub fn cooldown(mut self, duration: Duration) -> Self {
370 self.cooldown_duration = Some(duration);
371 self
372 }
373
374 #[cfg(feature = "tower")]
376 pub fn rate_limit(mut self, config: RateLimitConfig) -> Self {
377 self.rate_limit_config = Some(config);
378 self
379 }
380
381 #[cfg(feature = "tower")]
383 pub fn in_flight_limit(mut self, config: InFlightLimitConfig) -> Self {
384 self.in_flight_limit_config = Some(config);
385 self
386 }
387
388 #[cfg(feature = "tower")]
390 pub fn health_check(mut self, interval: Duration) -> Self {
391 self.health_check_interval = Some(interval);
392 self
393 }
394
395 #[cfg(feature = "tower")]
397 pub fn cost_tracking(mut self, enabled: bool) -> Self {
398 self.enable_cost_tracking = enabled;
399 self
400 }
401
402 #[cfg(feature = "tower")]
404 pub fn tracing(mut self, enabled: bool) -> Self {
405 self.enable_tracing = enabled;
406 self
407 }
408}
409
410#[cfg(any(feature = "native-http", feature = "wasm-http"))]
411impl ClientBuilder<WithApiKey, WithProvider> {
412 pub fn build(self) -> Result<super::DefaultClient> {
427 use super::config::ClientConfig;
428
429 let config = ClientConfig {
430 api_key: self.api_key,
431 base_url: self.base_url,
432 timeout: self.timeout,
433 connect_timeout: self.connect_timeout,
434 max_retries: self.max_retries,
435 max_response_bytes: self.max_response_bytes,
436 extra_headers: Vec::new(),
437 credential_provider: self.credential_provider,
438 load_env: self.load_env,
439 transport: self.transport,
440 bedrock_region: None,
441 bedrock_cross_region_prefix: None,
442 bedrock_access_key_id: None,
443 bedrock_secret_access_key: None,
444 bedrock_session_token: None,
445 #[cfg(feature = "tower")]
446 cache_config: self.cache_config,
447 #[cfg(feature = "tower")]
448 cache_store: self.cache_store,
449 #[cfg(feature = "tower")]
450 budget_config: self.budget_config,
451 #[cfg(feature = "tower")]
452 hooks: self.hooks,
453 #[cfg(feature = "tower")]
454 cooldown_duration: self.cooldown_duration,
455 #[cfg(feature = "tower")]
456 rate_limit_config: self.rate_limit_config,
457 #[cfg(feature = "tower")]
458 in_flight_limit_config: self.in_flight_limit_config,
459 #[cfg(feature = "tower")]
460 health_check_interval: self.health_check_interval,
461 #[cfg(feature = "tower")]
462 enable_cost_tracking: self.enable_cost_tracking,
463 #[cfg(feature = "tower")]
464 enable_tracing: self.enable_tracing,
465 };
466
467 let hint = if self.provider_hint.is_empty() {
468 None
469 } else {
470 Some(self.provider_hint.as_str())
471 };
472
473 super::DefaultClient::new(config, hint)
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use crate::client::ClientConfigBuilder;
481
482 #[test]
483 fn the_default_connect_timeout_is_ten_seconds() {
484 assert_eq!(ClientBuilder::new().connect_timeout, Duration::from_secs(10));
485 }
486
487 #[test]
488 fn the_typed_builder_and_the_config_builder_share_the_default_connect_timeout() {
489 assert_eq!(
490 ClientBuilder::new().connect_timeout,
491 ClientConfigBuilder::new("sk-test").build().connect_timeout
492 );
493 }
494
495 #[cfg(any(feature = "native-http", feature = "wasm-http"))]
499 #[test]
500 fn a_connect_timeout_set_before_api_key_reaches_the_client() {
501 let client = ClientBuilder::new()
502 .provider("openai")
503 .connect_timeout(Duration::from_secs(3))
504 .api_key("sk-test")
505 .build()
506 .expect("build the client");
507 assert_eq!(client.config.connect_timeout, Duration::from_secs(3));
508 }
509
510 #[cfg(any(feature = "native-http", feature = "wasm-http"))]
511 #[test]
512 fn a_connect_timeout_set_before_provider_reaches_the_client() {
513 let client = ClientBuilder::new()
514 .api_key("sk-test")
515 .connect_timeout(Duration::from_secs(3))
516 .provider("openai")
517 .build()
518 .expect("build the client");
519 assert_eq!(client.config.connect_timeout, Duration::from_secs(3));
520 }
521}