mod private
{
use crate::
{
error::{ ApiErrorWrap, HuggingFaceError, Result, map_deserialization_error },
};
#[ cfg( feature = "env-config" ) ]
use crate::environment::{ HuggingFaceEnvironment, EnvironmentInterface };
#[ cfg( feature = "inference" ) ]
use crate::inference::Inference;
#[ cfg( feature = "embeddings" ) ]
use crate::embeddings::Embeddings;
#[ cfg( feature = "models" ) ]
use crate::models::Models;
#[ cfg( feature = "inference" ) ]
use crate::providers::Providers;
#[ cfg( feature = "vision" ) ]
use crate::vision::Vision;
#[ cfg( feature = "audio" ) ]
use crate::audio::Audio;
use reqwest::
{
Client as HttpClient,
};
use serde::
{
de::DeserializeOwned,
Serialize,
};
#[ cfg( feature = "inference-streaming" ) ]
use tokio::sync::mpsc;
#[ cfg( feature = "inference-streaming" ) ]
use eventsource_stream::Eventsource;
#[ derive( Debug, Clone ) ]
pub struct Client< E >
where
E : Clone,
{
pub http_client : HttpClient,
pub environment : E,
}
#[ derive( Debug, Clone ) ]
pub struct ExplicitRetryConfig
{
pub max_retries : u32,
pub initial_delay_ms : u64,
pub multiplier : f64,
pub max_delay_ms : u64,
pub jitter_ms : u64,
}
impl ExplicitRetryConfig
{
#[ inline ]
#[ must_use ]
pub fn conservative() -> Self
{
Self
{
max_retries : 3,
initial_delay_ms : 1000,
multiplier : 2.0,
max_delay_ms : 30_000,
jitter_ms : 100,
}
}
#[ inline ]
#[ must_use ]
pub fn aggressive() -> Self
{
Self
{
max_retries : 5,
initial_delay_ms : 500,
multiplier : 1.5,
max_delay_ms : 10_000,
jitter_ms : 50,
}
}
}
#[ cfg( feature = "env-config" ) ]
impl< E > Client< E >
where
E : HuggingFaceEnvironment + EnvironmentInterface + Send + Sync + 'static + Clone,
{
#[ inline ]
pub fn build( environment : E ) -> Result< Self >
{
let headers = environment.headers()?;
let http_client = HttpClient::builder()
.default_headers( headers )
.build()
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )?;
Ok( Self
{
http_client,
environment,
} )
}
#[ cfg( feature = "inference" ) ]
#[ inline ]
#[ must_use ]
pub fn inference( &self ) -> Inference< E >
{
Inference::new( self )
}
#[ cfg( feature = "embeddings" ) ]
#[ inline ]
#[ must_use ]
pub fn embeddings( &self ) -> Embeddings< E >
{
Embeddings::new( self )
}
#[ cfg( feature = "models" ) ]
#[ inline ]
#[ must_use ]
pub fn models( &self ) -> Models< E >
{
Models::new( self )
}
#[ cfg( feature = "inference" ) ]
#[ inline ]
#[ must_use ]
pub fn providers( &self ) -> Providers< E >
{
Providers::new( self )
}
#[ cfg( feature = "vision" ) ]
#[ inline ]
#[ must_use ]
pub fn vision( &self ) -> Vision< E >
{
Vision::new( self )
}
#[ cfg( feature = "audio" ) ]
#[ inline ]
#[ must_use ]
pub fn audio( &self ) -> Audio< E >
{
Audio::new( self )
}
#[ inline ]
pub async fn post< T, R >( &self, url : &str, payload : &T ) -> Result< R >
where
T : Serialize + ?Sized,
R : DeserializeOwned,
{
self.post_direct( url, payload ).await
}
#[ inline ]
pub async fn post_bytes< T >( &self, url : &str, payload : &T ) -> Result< Vec< u8 > >
where
T : Serialize + ?Sized,
{
let response = self.http_client
.post( url )
.json( payload )
.send()
.await
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )?;
let status = response.status();
if !status.is_success()
{
let error_text = response.text().await
.unwrap_or_else( | _ | "Failed to read error response".to_string() );
return Err( HuggingFaceError::Api( ApiErrorWrap::new( error_text ).with_status_code( status.as_u16() ) ) );
}
response
.bytes()
.await
.map( | b | b.to_vec() )
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )
}
#[ inline ]
async fn post_direct< T, R >( &self, url : &str, payload : &T ) -> Result< R >
where
T : Serialize + ?Sized,
R : DeserializeOwned,
{
let response = self.http_client
.post( url )
.json( payload )
.send()
.await
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )?;
let status = response.status();
if status.is_success()
{
response
.json::< R >()
.await
.map_err( | e | HuggingFaceError::Serialization( e.to_string() ) )
} else {
let error_text = response.text().await
.unwrap_or_else( | _ | "Failed to read error response".to_string() );
Err( HuggingFaceError::Http( format!("HTTP {status} - {error_text}") ) )
}
}
#[ inline ]
pub async fn get< R >( &self, url : &str ) -> Result< R >
where
R : DeserializeOwned,
{
self.get_direct( url ).await
}
#[ inline ]
async fn get_direct< R >( &self, url : &str ) -> Result< R >
where
R : DeserializeOwned,
{
let response = self.http_client
.get( url )
.send()
.await
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )?;
let status = response.status();
if !status.is_success()
{
let error_text = response.text().await
.unwrap_or_else( | _ | "Failed to read error response".to_string() );
return Err( HuggingFaceError::Api( ApiErrorWrap::new( error_text ).with_status_code( status.as_u16() ) ) );
}
response
.json::< R >()
.await
.map_err( |e| map_deserialization_error( &e ) )
}
#[ inline ]
pub async fn post_with_explicit_retry< T, R >(
&self,
url : &str,
payload : &T,
retry_config : &ExplicitRetryConfig
) -> Result< R >
where
T : Serialize + ?Sized,
R : DeserializeOwned,
{
let mut retry_count = 0;
let mut delay = retry_config.initial_delay_ms;
loop
{
let result = self.post_direct( url, payload ).await;
match result
{
Ok( response ) => return Ok( response ),
Err( error ) if retry_count >= retry_config.max_retries => return Err( error ),
Err( error ) if Self::is_error_retryable( &error ) =>
{
retry_count += 1;
let jitter = ( rand::random::< u64 >() % ( retry_config.jitter_ms * 2 ) ).saturating_sub( retry_config.jitter_ms );
let total_delay = delay.saturating_add( jitter ).min( retry_config.max_delay_ms );
tokio::time::sleep( tokio::time::Duration::from_millis( total_delay ) ).await;
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
{
delay = ( ( delay as f64 ) * retry_config.multiplier ) as u64;
}
delay = delay.min( retry_config.max_delay_ms );
},
Err( error ) => return Err( error ), }
}
}
#[ inline ]
pub async fn get_with_explicit_retry< R >(
&self,
url : &str,
retry_config : &ExplicitRetryConfig
) -> Result< R >
where
R : DeserializeOwned,
{
let mut retry_count = 0;
let mut delay = retry_config.initial_delay_ms;
loop
{
let result = self.get_direct( url ).await;
match result
{
Ok( response ) => return Ok( response ),
Err( error ) if retry_count >= retry_config.max_retries => return Err( error ),
Err( error ) if Self::is_error_retryable( &error ) =>
{
retry_count += 1;
let jitter = ( rand::random::< u64 >() % ( retry_config.jitter_ms * 2 ) ).saturating_sub( retry_config.jitter_ms );
let total_delay = delay.saturating_add( jitter ).min( retry_config.max_delay_ms );
tokio::time::sleep( tokio::time::Duration::from_millis( total_delay ) ).await;
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
{
delay = ( ( delay as f64 ) * retry_config.multiplier ) as u64;
}
delay = delay.min( retry_config.max_delay_ms );
},
Err( error ) => return Err( error ), }
}
}
#[ inline ]
fn is_error_retryable( error : &HuggingFaceError ) -> bool
{
match error
{
HuggingFaceError::Http( _ ) |
HuggingFaceError::RateLimit( _ ) |
HuggingFaceError::ModelUnavailable( _ ) |
HuggingFaceError::Stream( _ ) => true,
HuggingFaceError::Api( api_error ) =>
{
if let Some( status_code ) = api_error.status_code
{
( 500..600 ).contains( &status_code )
}
else
{
let msg = api_error.message.to_lowercase();
msg.contains( "timeout" ) ||
msg.contains( "unavailable" ) ||
msg.contains( "overloaded" ) ||
msg.contains( "rate limit" ) ||
msg.contains( "service" )
}
},
HuggingFaceError::Authentication( _ ) |
HuggingFaceError::Validation( _ ) |
HuggingFaceError::Serialization( _ ) |
HuggingFaceError::InvalidArgument( _ ) |
HuggingFaceError::Generic( _ ) => false,
}
}
#[ cfg( feature = "inference-streaming" ) ]
#[ inline ]
pub async fn post_stream< T >( &self, url : &str, payload : &T ) -> Result< mpsc::Receiver< Result< String > > >
where
T : Serialize + ?Sized,
{
let response = self.http_client
.post( url )
.header( "Accept", "text/event-stream" )
.json( payload )
.send()
.await
.map_err( | e | HuggingFaceError::Http( e.to_string() ) )?;
let status = response.status();
if !status.is_success()
{
let error_text = response.text().await
.unwrap_or_else( | _ | "Failed to read error response".to_string() );
return Err( HuggingFaceError::Api( ApiErrorWrap::new( error_text ).with_status_code( status.as_u16() ) ) );
}
let ( tx, rx ) = mpsc::channel( 100 );
let byte_stream = response.bytes_stream();
let event_stream = byte_stream.eventsource();
tokio::spawn( async move
{
use futures_util::StreamExt;
let mut stream = event_stream;
while let Some( event ) = stream.next().await
{
match event
{
Ok( event ) =>
{
if (tx.send( Ok( event.data ) ).await).is_err()
{
break;
}
},
Err( e ) =>
{
let _ = tx.send( Err( HuggingFaceError::Stream( e.to_string() ) ) ).await;
break;
}
}
}
});
Ok( rx )
}
}
#[ cfg( not( feature = "env-config" ) ) ]
impl< E > Client< E >
where
E : Clone,
{
#[ inline ]
pub fn build( environment : E ) -> Result< Self >
{
let http_client = HttpClient::new();
Ok( Self
{
http_client,
environment,
} )
}
#[ cfg( feature = "inference" ) ]
#[ inline ]
#[ must_use ]
pub fn inference( &self ) -> Inference< E >
{
Inference::new( self )
}
#[ cfg( feature = "embeddings" ) ]
#[ inline ]
#[ must_use ]
pub fn embeddings( &self ) -> Embeddings< E >
{
Embeddings::new( self )
}
#[ cfg( feature = "models" ) ]
#[ inline ]
#[ must_use ]
pub fn models( &self ) -> Models< E >
{
Models::new( self )
}
#[ cfg( feature = "inference" ) ]
#[ inline ]
#[ must_use ]
pub fn providers( &self ) -> Providers< E >
{
Providers::new( self )
}
#[ cfg( feature = "vision" ) ]
#[ inline ]
#[ must_use ]
pub fn vision( &self ) -> Vision< E >
{
Vision::new( self )
}
#[ cfg( feature = "audio" ) ]
#[ inline ]
#[ must_use ]
pub fn audio( &self ) -> Audio< E >
{
Audio::new( self )
}
}
}
crate::mod_interface!
{
exposed use private::Client;
exposed use private::ExplicitRetryConfig;
}