1use dashmap::DashMap;
26use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
27use reqwest::{
28 Body, Client, ClientBuilder, IntoUrl, Method, RequestBuilder, Version, multipart, redirect,
29};
30use serde::Serialize;
31use std::error::Error as _;
32use std::io;
33use std::net::{SocketAddr, ToSocketAddrs};
34use std::sync::{Arc, OnceLock};
35use std::time::Duration;
36use tracing::{debug, warn};
37
38use crate::core::net::{
39 ProviderEndpointAccess, ProviderEndpointPolicy, SsrfError, is_provider_endpoint_ip_allowed,
40 validate_outbound_url_without_resolution,
41};
42
43trait HostResolver: Send + Sync {
49 fn resolve(&self, host: &str) -> io::Result<Vec<SocketAddr>>;
50}
51
52struct SystemHostResolver;
53
54impl HostResolver for SystemHostResolver {
55 fn resolve(&self, host: &str) -> io::Result<Vec<SocketAddr>> {
56 (host, 0u16)
57 .to_socket_addrs()
58 .map(|addresses| addresses.collect())
59 }
60}
61
62struct PolicyDnsResolver {
63 access: ProviderEndpointAccess,
64 resolver: Arc<dyn HostResolver>,
65}
66
67#[derive(Debug, thiserror::Error)]
68enum ProviderEndpointPolicyRequestError {
69 #[error("Host resolution returned no addresses (SSRF protection)")]
70 EmptyDnsAnswer,
71 #[error("Host resolves to a disallowed address (SSRF protection)")]
72 DisallowedDnsAddress,
73 #[error("Redirect target failed SSRF validation")]
74 RedirectTarget,
75}
76
77fn reqwest_error_is_endpoint_policy(error: &reqwest::Error) -> bool {
78 let mut source = error.source();
79 while let Some(current) = source {
80 if current
81 .downcast_ref::<ProviderEndpointPolicyRequestError>()
82 .is_some()
83 || current
84 .downcast_ref::<io::Error>()
85 .and_then(io::Error::get_ref)
86 .and_then(|inner| inner.downcast_ref::<ProviderEndpointPolicyRequestError>())
87 .is_some()
88 {
89 return true;
90 }
91 source = current.source();
92 }
93 false
94}
95
96impl reqwest::dns::Resolve for PolicyDnsResolver {
97 fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
98 let host = name.as_str().to_owned();
99 let resolver = Arc::clone(&self.resolver);
100 let access = self.access;
101 Box::pin(async move {
102 let addrs = tokio::task::spawn_blocking(move || resolver.resolve(&host))
103 .await
104 .map_err(io::Error::other)??;
105 let safe = validate_provider_addresses(access, addrs)?;
106
107 Ok(Box::new(safe.into_iter()) as reqwest::dns::Addrs)
108 })
109 }
110}
111
112fn validate_provider_addresses(
113 access: ProviderEndpointAccess,
114 addrs: Vec<SocketAddr>,
115) -> io::Result<Vec<SocketAddr>> {
116 if addrs.is_empty() {
117 return Err(io::Error::other(
118 ProviderEndpointPolicyRequestError::EmptyDnsAnswer,
119 ));
120 }
121 if addrs
122 .iter()
123 .any(|addr| !is_provider_endpoint_ip_allowed(access, &addr.ip()))
124 {
125 return Err(io::Error::other(
126 ProviderEndpointPolicyRequestError::DisallowedDnsAddress,
127 ));
128 }
129 Ok(addrs)
130}
131
132fn ssrf_safe_redirect_policy() -> redirect::Policy {
133 redirect::Policy::custom(|attempt| {
134 if validate_outbound_url_without_resolution(attempt.url()).is_err() {
135 debug!("blocked redirect target by outbound endpoint policy");
136 return attempt.error(ProviderEndpointPolicyRequestError::RedirectTarget);
137 }
138
139 redirect::Policy::limited(10).redirect(attempt)
140 })
141}
142
143fn provider_redirect_policy(policy: ProviderEndpointPolicy) -> redirect::Policy {
144 if policy.access() == ProviderEndpointAccess::PrivateNetwork {
145 return redirect::Policy::none();
146 }
147
148 redirect::Policy::custom(move |attempt| {
149 if policy
150 .validate_url_without_resolution(attempt.url())
151 .is_err()
152 {
153 debug!("blocked provider redirect target by endpoint policy");
154 return attempt.error(ProviderEndpointPolicyRequestError::RedirectTarget);
155 }
156 redirect::Policy::limited(10).redirect(attempt)
157 })
158}
159
160#[derive(Debug, thiserror::Error)]
161pub enum ProviderHttpClientError {
162 #[error(transparent)]
163 Endpoint(#[from] SsrfError),
164 #[error(transparent)]
165 Request(#[from] reqwest::Error),
166}
167
168impl ProviderHttpClientError {
169 pub(crate) fn is_endpoint_policy(&self) -> bool {
170 matches!(self, Self::Endpoint(_))
171 }
172}
173
174pub struct ProviderRequestBuilder {
176 inner: RequestBuilder,
177}
178
179impl ProviderRequestBuilder {
180 fn map(self, transform: impl FnOnce(RequestBuilder) -> RequestBuilder) -> Self {
181 Self {
182 inner: transform(self.inner),
183 }
184 }
185
186 pub fn header<K, V>(self, key: K, value: V) -> Self
187 where
188 HeaderName: TryFrom<K>,
189 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
190 HeaderValue: TryFrom<V>,
191 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
192 {
193 self.map(|inner| inner.header(key, value))
194 }
195
196 pub fn headers(self, headers: HeaderMap) -> Self {
197 self.map(|inner| inner.headers(headers))
198 }
199
200 pub fn basic_auth<U: std::fmt::Display, P: std::fmt::Display>(
201 self,
202 username: U,
203 password: Option<P>,
204 ) -> Self {
205 self.map(|inner| inner.basic_auth(username, password))
206 }
207
208 pub fn bearer_auth<T: std::fmt::Display>(self, token: T) -> Self {
209 self.map(|inner| inner.bearer_auth(token))
210 }
211
212 pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
213 self.map(|inner| inner.query(query))
214 }
215
216 pub fn body<T: Into<Body>>(self, body: T) -> Self {
217 self.map(|inner| inner.body(body))
218 }
219
220 pub fn timeout(self, timeout: Duration) -> Self {
221 self.map(|inner| inner.timeout(timeout))
222 }
223
224 pub fn version(self, version: Version) -> Self {
225 self.map(|inner| inner.version(version))
226 }
227
228 pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
229 self.map(|inner| inner.form(form))
230 }
231
232 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
233 self.map(|inner| inner.json(json))
234 }
235
236 pub fn multipart(self, form: multipart::Form) -> Self {
237 self.map(|inner| inner.multipart(form))
238 }
239
240 pub async fn send(self) -> Result<reqwest::Response, reqwest::Error> {
241 self.inner.send().await
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246enum ProviderClientMode {
247 Request,
248 Streaming,
249 StreamingNoRedirect,
250 NoRedirect,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Hash)]
254struct ProviderClientCacheKey {
255 policy: ProviderEndpointPolicy,
256 timeout: Duration,
257 mode: ProviderClientMode,
258}
259
260static PROVIDER_CLIENT_CACHE: OnceLock<DashMap<ProviderClientCacheKey, Arc<Client>>> =
261 OnceLock::new();
262
263#[derive(Clone)]
265pub struct ProviderHttpClient {
266 client: Arc<Client>,
267 policy: ProviderEndpointPolicy,
268}
269
270impl std::fmt::Debug for ProviderHttpClient {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 f.debug_struct("ProviderHttpClient")
273 .field("policy", &self.policy)
274 .finish_non_exhaustive()
275 }
276}
277
278impl ProviderHttpClient {
279 pub(crate) fn request_error_is_endpoint_policy(error: &reqwest::Error) -> bool {
280 reqwest_error_is_endpoint_policy(error)
281 }
282
283 pub fn new(
284 policy: ProviderEndpointPolicy,
285 timeout: Duration,
286 ) -> Result<Self, ProviderHttpClientError> {
287 Self::cached(policy, timeout, ProviderClientMode::Request)
288 }
289
290 pub fn streaming(policy: ProviderEndpointPolicy) -> Result<Self, ProviderHttpClientError> {
291 Self::cached(
292 policy,
293 Duration::from_secs(0),
294 ProviderClientMode::Streaming,
295 )
296 }
297
298 pub fn streaming_no_redirect(
299 policy: ProviderEndpointPolicy,
300 ) -> Result<Self, ProviderHttpClientError> {
301 Self::cached(
302 policy,
303 Duration::from_secs(0),
304 ProviderClientMode::StreamingNoRedirect,
305 )
306 }
307
308 pub fn no_redirect(
309 policy: ProviderEndpointPolicy,
310 timeout: Duration,
311 ) -> Result<Self, ProviderHttpClientError> {
312 Self::cached(policy, timeout, ProviderClientMode::NoRedirect)
313 }
314
315 fn cached(
316 policy: ProviderEndpointPolicy,
317 timeout: Duration,
318 mode: ProviderClientMode,
319 ) -> Result<Self, ProviderHttpClientError> {
320 let key = ProviderClientCacheKey {
321 policy: policy.clone(),
322 timeout,
323 mode,
324 };
325 let client = PROVIDER_CLIENT_CACHE
326 .get_or_init(DashMap::new)
327 .entry(key)
328 .or_try_insert_with(|| {
329 Self::build_client(&policy, timeout, mode, Arc::new(SystemHostResolver))
330 .map(Arc::new)
331 })?
332 .clone();
333 Ok(Self { client, policy })
334 }
335
336 #[cfg(test)]
337 fn build(
338 policy: ProviderEndpointPolicy,
339 timeout: Duration,
340 mode: ProviderClientMode,
341 resolver: Arc<dyn HostResolver>,
342 ) -> Result<Self, ProviderHttpClientError> {
343 let client = Arc::new(Self::build_client(&policy, timeout, mode, resolver)?);
344 Ok(Self { client, policy })
345 }
346
347 fn build_client(
348 policy: &ProviderEndpointPolicy,
349 timeout: Duration,
350 mode: ProviderClientMode,
351 resolver: Arc<dyn HostResolver>,
352 ) -> Result<Client, ProviderHttpClientError> {
353 let resolver = Arc::new(PolicyDnsResolver {
354 access: policy.access(),
355 resolver,
356 });
357 Self::build_client_with_dns_resolver(policy, timeout, mode, resolver)
358 }
359
360 fn build_client_with_dns_resolver<R: reqwest::dns::Resolve + 'static>(
361 policy: &ProviderEndpointPolicy,
362 timeout: Duration,
363 mode: ProviderClientMode,
364 resolver: Arc<R>,
365 ) -> Result<Client, ProviderHttpClientError> {
366 let config = HttpClientPoolConfig::default();
367 let builder = match mode {
368 ProviderClientMode::Streaming | ProviderClientMode::StreamingNoRedirect => {
369 { ClientBuilder::new() }
370 .pool_max_idle_per_host(config.pool_max_idle_per_host)
371 .pool_idle_timeout(config.pool_idle_timeout)
372 .connect_timeout(config.connect_timeout)
373 .tcp_keepalive(config.tcp_keepalive)
374 .tcp_nodelay(true)
375 .user_agent(config.user_agent)
376 }
377 ProviderClientMode::Request | ProviderClientMode::NoRedirect => {
378 create_client_builder_with_config(timeout, &config)
379 }
380 };
381 let redirect_policy = if matches!(
382 mode,
383 ProviderClientMode::NoRedirect | ProviderClientMode::StreamingNoRedirect
384 ) {
385 redirect::Policy::none()
386 } else {
387 provider_redirect_policy(policy.clone())
388 };
389 let client = builder
390 .no_proxy()
391 .dns_resolver(resolver)
392 .redirect(redirect_policy)
393 .build()?;
394 Ok(client)
395 }
396
397 pub fn request<U: IntoUrl>(
398 &self,
399 method: Method,
400 url: U,
401 ) -> Result<ProviderRequestBuilder, ProviderHttpClientError> {
402 let url = url.into_url()?;
403 self.policy.validate_url_without_resolution(&url)?;
404 Ok(ProviderRequestBuilder {
405 inner: self.client.request(method, url),
406 })
407 }
408
409 pub fn get<U: IntoUrl>(
410 &self,
411 url: U,
412 ) -> Result<ProviderRequestBuilder, ProviderHttpClientError> {
413 self.request(Method::GET, url)
414 }
415
416 pub fn post<U: IntoUrl>(
417 &self,
418 url: U,
419 ) -> Result<ProviderRequestBuilder, ProviderHttpClientError> {
420 self.request(Method::POST, url)
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct HttpClientPoolConfig {
427 pub pool_max_idle_per_host: usize,
429 pub pool_idle_timeout: Duration,
431 pub connect_timeout: Duration,
433 pub tcp_keepalive: Duration,
435 pub user_agent: &'static str,
437}
438
439impl Default for HttpClientPoolConfig {
440 fn default() -> Self {
441 Self {
442 pool_max_idle_per_host: 100, pool_idle_timeout: Duration::from_secs(90),
444 connect_timeout: Duration::from_secs(10),
445 tcp_keepalive: Duration::from_secs(60),
446 user_agent: "LiteLLM-RS/0.1.0",
447 }
448 }
449}
450
451static SHARED_HTTP_CLIENT: OnceLock<Client> = OnceLock::new();
453
454static TIMEOUT_CLIENT_CACHE: OnceLock<DashMap<u64, Arc<Client>>> = OnceLock::new();
456
457static SSRF_SAFE_TIMEOUT_CLIENT_CACHE: OnceLock<DashMap<u64, Arc<Client>>> = OnceLock::new();
459
460pub fn create_client_builder_with_config(
462 timeout: Duration,
463 config: &HttpClientPoolConfig,
464) -> ClientBuilder {
465 ClientBuilder::new()
466 .pool_max_idle_per_host(config.pool_max_idle_per_host)
468 .pool_idle_timeout(config.pool_idle_timeout)
469 .timeout(timeout)
471 .connect_timeout(config.connect_timeout)
472 .tcp_keepalive(config.tcp_keepalive)
474 .tcp_nodelay(true)
475 .user_agent(config.user_agent)
477}
478
479pub fn create_client_builder(timeout: Duration) -> ClientBuilder {
481 create_client_builder_with_config(timeout, &HttpClientPoolConfig::default())
482}
483
484pub fn get_shared_client() -> &'static Client {
489 SHARED_HTTP_CLIENT.get_or_init(|| {
490 debug!("Initializing shared HTTP client with optimized settings");
491 create_optimized_client(Duration::from_secs(30))
492 })
493}
494
495pub fn get_client_with_timeout(timeout: Duration) -> Arc<Client> {
500 let cache = TIMEOUT_CLIENT_CACHE.get_or_init(DashMap::new);
501 let timeout_millis = timeout.as_millis().min(u64::MAX as u128) as u64;
502
503 cache
504 .entry(timeout_millis)
505 .or_insert_with(|| {
506 debug!(timeout_millis, "Creating cached HTTP client for timeout");
507 Arc::new(create_optimized_client(timeout))
508 })
509 .clone()
510}
511
512pub fn get_client_with_timeout_fallible(timeout: Duration) -> Result<Arc<Client>, reqwest::Error> {
516 let cache = TIMEOUT_CLIENT_CACHE.get_or_init(DashMap::new);
517 let timeout_millis = timeout.as_millis().min(u64::MAX as u128) as u64;
518
519 if let Some(existing) = cache.get(&timeout_millis) {
520 return Ok(existing.clone());
521 }
522
523 let client = Arc::new(create_custom_client(timeout)?);
524 cache.insert(timeout_millis, client.clone());
525 Ok(client)
526}
527
528fn create_optimized_client(timeout: Duration) -> Client {
530 let config = HttpClientPoolConfig::default();
531
532 create_client_builder_with_config(timeout, &config)
533 .build()
534 .unwrap_or_else(|e| {
535 warn!(
536 "Failed to create optimized HTTP client, falling back to default: {}",
537 e
538 );
539 Client::builder()
540 .build()
541 .expect("default HTTP client should build")
542 })
543}
544
545pub fn create_custom_client_with_config(
547 timeout: Duration,
548 config: &HttpClientPoolConfig,
549) -> Result<Client, reqwest::Error> {
550 create_client_builder_with_config(timeout, config).build()
551}
552
553pub fn create_custom_client(timeout: Duration) -> Result<Client, reqwest::Error> {
558 create_custom_client_with_config(timeout, &HttpClientPoolConfig::default())
559}
560
561pub fn create_streaming_client() -> Result<Client, reqwest::Error> {
567 let config = HttpClientPoolConfig::default();
568 ClientBuilder::new()
569 .pool_max_idle_per_host(config.pool_max_idle_per_host)
570 .pool_idle_timeout(config.pool_idle_timeout)
571 .connect_timeout(config.connect_timeout)
572 .tcp_keepalive(config.tcp_keepalive)
573 .tcp_nodelay(true)
574 .user_agent(config.user_agent)
575 .build()
576}
577
578pub fn get_ssrf_safe_client_with_timeout_fallible(
584 timeout: Duration,
585) -> Result<Arc<Client>, reqwest::Error> {
586 let cache = SSRF_SAFE_TIMEOUT_CLIENT_CACHE.get_or_init(DashMap::new);
587 let timeout_millis = timeout.as_millis().min(u64::MAX as u128) as u64;
588
589 if let Some(existing) = cache.get(&timeout_millis) {
590 return Ok(existing.clone());
591 }
592
593 let client = Arc::new(create_ssrf_safe_client(
594 timeout,
595 ssrf_safe_redirect_policy(),
596 )?);
597 cache.insert(timeout_millis, client.clone());
598 Ok(client)
599}
600
601fn create_ssrf_safe_client(
602 timeout: Duration,
603 redirect_policy: redirect::Policy,
604) -> Result<Client, reqwest::Error> {
605 create_client_builder_with_config(timeout, &HttpClientPoolConfig::default())
606 .no_proxy()
607 .dns_resolver(Arc::new(PolicyDnsResolver {
608 access: ProviderEndpointAccess::PublicOnly,
609 resolver: Arc::new(SystemHostResolver),
610 }))
611 .redirect(redirect_policy)
612 .build()
613}
614
615pub fn create_custom_client_with_headers(
617 timeout: Duration,
618 default_headers: reqwest::header::HeaderMap,
619) -> Result<Client, reqwest::Error> {
620 create_client_builder(timeout)
621 .default_headers(default_headers)
622 .build()
623}
624
625pub fn get_cache_stats() -> HttpClientCacheStats {
627 let cache = TIMEOUT_CLIENT_CACHE.get_or_init(DashMap::new);
628 HttpClientCacheStats {
629 cached_clients: cache.len(),
630 timeout_configs: cache.iter().map(|e| *e.key()).collect(),
631 }
632}
633
634#[derive(Debug, Clone)]
636pub struct HttpClientCacheStats {
637 pub cached_clients: usize,
639 pub timeout_configs: Vec<u64>,
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646 use std::net::{Ipv4Addr, SocketAddr};
647 use tokio::io::{AsyncReadExt, AsyncWriteExt};
648
649 #[test]
650 fn test_shared_client_creation() {
651 let client = get_shared_client();
652 assert!(std::ptr::addr_of!(*client) == std::ptr::addr_of!(*get_shared_client()));
654 }
655
656 #[test]
657 fn test_custom_client_creation() {
658 let client = create_custom_client(Duration::from_secs(15));
659 assert!(client.is_ok());
660 }
661
662 #[test]
663 fn test_client_with_timeout_caching() {
664 let client1 = get_client_with_timeout(Duration::from_secs(60));
665 let client2 = get_client_with_timeout(Duration::from_secs(60));
666
667 assert!(Arc::ptr_eq(&client1, &client2));
669
670 let client3 = get_client_with_timeout(Duration::from_secs(120));
672 assert!(!Arc::ptr_eq(&client1, &client3));
673 }
674
675 #[test]
676 fn test_client_with_timeout_fallible_caching() {
677 let client1 = get_client_with_timeout_fallible(Duration::from_millis(1500)).unwrap();
678 let client2 = get_client_with_timeout_fallible(Duration::from_millis(1500)).unwrap();
679
680 assert!(Arc::ptr_eq(&client1, &client2));
681 }
682
683 #[test]
684 fn test_ssrf_safe_dns_filter_rejects_private_and_reserved_addresses() {
685 let addrs = vec![
686 SocketAddr::from((Ipv4Addr::new(93, 184, 216, 34), 443)),
687 SocketAddr::from((Ipv4Addr::new(198, 18, 0, 1), 443)),
688 SocketAddr::from((Ipv4Addr::new(224, 0, 0, 1), 443)),
689 SocketAddr::from((Ipv4Addr::new(240, 0, 0, 1), 443)),
690 ];
691
692 assert!(
693 validate_provider_addresses(ProviderEndpointAccess::PublicOnly, addrs).is_err(),
694 "a mixed DNS answer must fail as a whole"
695 );
696 }
697
698 #[test]
699 fn test_ssrf_safe_client_with_timeout_fallible_caching() {
700 let client1 = match get_ssrf_safe_client_with_timeout_fallible(Duration::from_millis(1500))
701 {
702 Ok(client) => client,
703 Err(error) => panic!("SSRF-safe client should build: {error}"),
704 };
705 let client2 = match get_ssrf_safe_client_with_timeout_fallible(Duration::from_millis(1500))
706 {
707 Ok(client) => client,
708 Err(error) => panic!("SSRF-safe client should build: {error}"),
709 };
710
711 assert!(Arc::ptr_eq(&client1, &client2));
712 }
713
714 #[tokio::test]
715 async fn test_ssrf_safe_redirect_policy_rejects_private_redirect_targets()
716 -> Result<(), Box<dyn std::error::Error>> {
717 let source = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
718 let source_address = source.local_addr()?;
719 let target = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
720 let target_address = target.local_addr()?;
721
722 let server = tokio::spawn(async move {
723 let (mut stream, _) = source.accept().await?;
724 let mut buffer = [0_u8; 1024];
725 let bytes_read = stream.read(&mut buffer).await?;
726 assert!(bytes_read > 0);
727
728 let location = format!("http://{target_address}/private");
729 let body = "redirect";
730 let response = format!(
731 "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
732 body.len()
733 );
734 stream.write_all(response.as_bytes()).await?;
735 Ok::<(), std::io::Error>(())
736 });
737
738 let client = ClientBuilder::new()
739 .redirect(ssrf_safe_redirect_policy())
740 .build()?;
741 let result = client
742 .get(format!("http://{source_address}/redirect"))
743 .send()
744 .await;
745
746 let error = match result {
747 Ok(response) => panic!(
748 "private redirect target should be rejected, got status {}",
749 response.status()
750 ),
751 Err(error) => error,
752 };
753 assert!(error.is_redirect(), "{error:?}");
754 assert!(reqwest_error_is_endpoint_policy(&error), "{error:?}");
755 assert!(
756 tokio::time::timeout(Duration::from_millis(100), target.accept())
757 .await
758 .is_err(),
759 "blocked redirect must not open the target socket"
760 );
761 server.await??;
762 Ok(())
763 }
764
765 #[test]
766 fn test_cache_stats() {
767 let _ = get_client_with_timeout(Duration::from_secs(30));
769 let _ = get_client_with_timeout(Duration::from_secs(45));
770
771 let stats = get_cache_stats();
772 assert!(stats.cached_clients >= 2);
773 assert!(stats.timeout_configs.contains(&30_000));
774 assert!(stats.timeout_configs.contains(&45_000));
775 }
776
777 #[test]
778 fn test_pool_config_defaults() {
779 let config = HttpClientPoolConfig::default();
780 assert_eq!(config.pool_max_idle_per_host, 100);
781 assert_eq!(config.pool_idle_timeout, Duration::from_secs(90));
782 assert_eq!(config.connect_timeout, Duration::from_secs(10));
783 assert_eq!(config.tcp_keepalive, Duration::from_secs(60));
784 assert_eq!(config.user_agent, "LiteLLM-RS/0.1.0");
785 }
786}
787
788#[cfg(test)]
789mod provider_tests;