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