Skip to main content

rig_core/providers/huggingface/
client.rs

1use 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    /// Get the chat completion endpoint for the SubProvider
27    /// Required because Huggingface Inference requires the model
28    /// in the url and in the request body.
29    pub fn completion_endpoint(&self, _model: &str) -> String {
30        "v1/chat/completions".to_string()
31    }
32
33    /// Get the transcription endpoint for the SubProvider
34    /// Required because Huggingface Inference requires the model
35    /// in the url and in the request body.
36    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    /// Get the image generation endpoint for the SubProvider
46    /// Required because Huggingface Inference requires the model
47    /// in the url and in the request body.
48    #[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            // Fireworks addresses models by a fully-qualified id. Guard against
61            // re-prefixing an already-qualified id (e.g. a per-request model
62            // override that is already fully qualified) — the generic path
63            // applies this to the resolved request model unconditionally, so
64            // without the guard a qualified override would become an invalid
65            // `accounts/fireworks/models/accounts/fireworks/models/...` id.
66            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
108// ================================================================
109// Main Huggingface Client
110// ================================================================
111const 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    // Structured-output support varies by sub-provider; keep the
141    // pre-migration behavior of dropping `output_schema` with a warning.
142    const SUPPORTS_RESPONSE_FORMAT: bool = false;
143
144    type Response = crate::providers::openai::CompletionResponse;
145
146    // Chat completions live under the router's `/v1` while verification,
147    // transcription, and image generation use root-relative paths, so the
148    // prefix cannot live in the client base URL.
149    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        // Some sub-providers (Fireworks) address models through a qualified
158        // identifier in the request body.
159        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    /// Create a new Huggingface client from the `HUGGINGFACE_API_KEY` environment variable.
209    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        // A bare id is qualified once...
249        assert_eq!(
250            SubProvider::Fireworks.model_identifier("deepseek-v3"),
251            "accounts/fireworks/models/deepseek-v3"
252        );
253        // ...and an already-qualified id (e.g. a per-request model override)
254        // is left untouched rather than double-prefixed.
255        assert_eq!(
256            SubProvider::Fireworks.model_identifier("accounts/fireworks/models/deepseek-v3"),
257            "accounts/fireworks/models/deepseek-v3"
258        );
259        // Other sub-providers pass the id through verbatim.
260        assert_eq!(
261            SubProvider::HFInference.model_identifier("meta-llama/Llama-3.1-8B"),
262            "meta-llama/Llama-3.1-8B"
263        );
264    }
265}