rig_core/providers/huggingface/
client.rs1use crate::client::{
2 self, BearerAuth, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder,
3 ProviderClient,
4};
5use crate::http_client;
6#[cfg(feature = "image")]
7use crate::image_generation::ImageGenerationError;
8use crate::transcription::TranscriptionError;
9use std::fmt::Debug;
10use std::fmt::Display;
11
12#[derive(Debug, Clone, PartialEq, Default)]
13pub enum SubProvider {
14 #[default]
15 HFInference,
16 Together,
17 SambaNova,
18 Fireworks,
19 Hyperbolic,
20 Nebius,
21 Novita,
22 Custom(String),
23}
24
25impl SubProvider {
26 pub fn completion_endpoint(&self, _model: &str) -> String {
30 "v1/chat/completions".to_string()
31 }
32
33 pub fn transcription_endpoint(&self, model: &str) -> Result<String, TranscriptionError> {
37 match self {
38 SubProvider::HFInference => Ok(format!("/{model}")),
39 _ => Err(TranscriptionError::ProviderError(format!(
40 "transcription endpoint is not supported yet for {self}"
41 ))),
42 }
43 }
44
45 #[cfg(feature = "image")]
49 pub fn image_generation_endpoint(&self, model: &str) -> Result<String, ImageGenerationError> {
50 match self {
51 SubProvider::HFInference => Ok(format!("/{model}")),
52 _ => Err(ImageGenerationError::ProviderError(format!(
53 "image generation endpoint is not supported yet for {self}"
54 ))),
55 }
56 }
57
58 pub fn model_identifier(&self, model: &str) -> String {
59 match self {
60 SubProvider::Fireworks => {
67 const FIREWORKS_PREFIX: &str = "accounts/fireworks/models/";
68 if model.starts_with(FIREWORKS_PREFIX) {
69 model.to_string()
70 } else {
71 format!("{FIREWORKS_PREFIX}{model}")
72 }
73 }
74 _ => model.to_string(),
75 }
76 }
77}
78
79impl From<&str> for SubProvider {
80 fn from(s: &str) -> Self {
81 SubProvider::Custom(s.to_string())
82 }
83}
84
85impl From<String> for SubProvider {
86 fn from(value: String) -> Self {
87 SubProvider::Custom(value)
88 }
89}
90
91impl Display for SubProvider {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 let route = match self {
94 SubProvider::HFInference => "hf-inference/models".to_string(),
95 SubProvider::Together => "together".to_string(),
96 SubProvider::SambaNova => "sambanova".to_string(),
97 SubProvider::Fireworks => "fireworks-ai".to_string(),
98 SubProvider::Hyperbolic => "hyperbolic".to_string(),
99 SubProvider::Nebius => "nebius".to_string(),
100 SubProvider::Novita => "novita".to_string(),
101 SubProvider::Custom(route) => route.clone(),
102 };
103
104 write!(f, "{route}")
105 }
106}
107
108const HUGGINGFACE_API_BASE_URL: &str = "https://router.huggingface.co";
112
113#[derive(Debug, Default, Clone)]
114pub struct HuggingFaceExt {
115 subprovider: SubProvider,
116}
117
118#[derive(Debug, Default, Clone)]
119pub struct HuggingFaceBuilder {
120 subprovider: SubProvider,
121}
122
123type HuggingFaceApiKey = BearerAuth;
124
125pub type Client<H = reqwest::Client> = client::Client<HuggingFaceExt, H>;
126pub type ClientBuilder<H = crate::markers::Missing> =
127 client::ClientBuilder<HuggingFaceBuilder, HuggingFaceApiKey, H>;
128
129impl Provider for HuggingFaceExt {
130 type Builder = HuggingFaceBuilder;
131
132 const VERIFY_PATH: &'static str = "/api/whoami-v2";
133}
134
135impl crate::providers::openai::completion::OpenAICompatibleProvider for HuggingFaceExt {
136 const PROVIDER_NAME: &'static str = "huggingface";
137
138 type StreamingUsage = crate::providers::openai::Usage;
139
140 const SUPPORTS_RESPONSE_FORMAT: bool = false;
143
144 type Response = crate::providers::openai::CompletionResponse;
145
146 fn completion_path(&self, _model: &str) -> String {
150 self.subprovider.completion_endpoint(_model)
151 }
152
153 fn prepare_request(
154 &self,
155 request: &mut crate::providers::openai::completion::CompletionRequest,
156 ) -> Result<(), crate::completion::CompletionError> {
157 request.model = self.subprovider.model_identifier(&request.model);
160 Ok(())
161 }
162}
163
164impl<H> Capabilities<H> for HuggingFaceExt {
165 type Completion = Capable<super::completion::CompletionModel<H>>;
166 type Embeddings = Nothing;
167 type Transcription = Capable<super::transcription::TranscriptionModel<H>>;
168 type ModelListing = Nothing;
169 #[cfg(feature = "image")]
170 type ImageGeneration = Capable<super::image_generation::ImageGenerationModel<H>>;
171
172 #[cfg(feature = "audio")]
173 type AudioGeneration = Nothing;
174 type Rerank = Nothing;
175}
176
177impl DebugExt for HuggingFaceExt {
178 fn fields(&self) -> impl Iterator<Item = (&'static str, &dyn Debug)> {
179 std::iter::once(("subprovider", (&self.subprovider as &dyn Debug)))
180 }
181}
182
183impl ProviderBuilder for HuggingFaceBuilder {
184 type Extension<H>
185 = HuggingFaceExt
186 where
187 H: http_client::HttpClientExt;
188 type ApiKey = HuggingFaceApiKey;
189
190 const BASE_URL: &'static str = HUGGINGFACE_API_BASE_URL;
191
192 fn build<H>(
193 builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
194 ) -> http_client::Result<Self::Extension<H>>
195 where
196 H: http_client::HttpClientExt,
197 {
198 Ok(HuggingFaceExt {
199 subprovider: builder.ext().subprovider.clone(),
200 })
201 }
202}
203
204impl ProviderClient for Client {
205 type Input = String;
206 type Error = crate::client::ProviderClientError;
207
208 fn from_env() -> Result<Self, Self::Error> {
210 let api_key = crate::client::required_env_var("HUGGINGFACE_API_KEY")?;
211
212 Self::new(&api_key).map_err(Into::into)
213 }
214
215 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
216 Self::new(&input).map_err(Into::into)
217 }
218}
219
220impl<H> ClientBuilder<H> {
221 pub fn subprovider(mut self, subprovider: SubProvider) -> Self {
222 *self.ext_mut() = HuggingFaceBuilder { subprovider };
223 self
224 }
225}
226
227impl<H> Client<H> {
228 pub(crate) fn subprovider(&self) -> &SubProvider {
229 &self.ext().subprovider
230 }
231}
232#[cfg(test)]
233mod tests {
234 use super::SubProvider;
235
236 #[test]
237 fn test_client_initialization() {
238 let _client =
239 crate::providers::huggingface::Client::new("dummy-key").expect("Client::new() failed");
240 let _client_from_builder = crate::providers::huggingface::Client::builder()
241 .api_key("dummy-key")
242 .build()
243 .expect("Client::builder() failed");
244 }
245
246 #[test]
247 fn fireworks_model_identifier_is_idempotent() {
248 assert_eq!(
250 SubProvider::Fireworks.model_identifier("deepseek-v3"),
251 "accounts/fireworks/models/deepseek-v3"
252 );
253 assert_eq!(
256 SubProvider::Fireworks.model_identifier("accounts/fireworks/models/deepseek-v3"),
257 "accounts/fireworks/models/deepseek-v3"
258 );
259 assert_eq!(
261 SubProvider::HFInference.model_identifier("meta-llama/Llama-3.1-8B"),
262 "meta-llama/Llama-3.1-8B"
263 );
264 }
265}