Skip to main content

rig_core/providers/huggingface/
client.rs

1use 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    /// Get the chat completion endpoint for the SubProvider
24    /// Required because Huggingface Inference requires the model
25    /// in the url and in the request body.
26    pub fn completion_endpoint(&self, _model: &str) -> String {
27        "v1/chat/completions".to_string()
28    }
29
30    /// Get the transcription endpoint for the SubProvider
31    /// Required because Huggingface Inference requires the model
32    /// in the url and in the request body.
33    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    /// Get the image generation endpoint for the SubProvider
43    /// Required because Huggingface Inference requires the model
44    /// in the url and in the request body.
45    #[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            // Fireworks addresses models by a fully-qualified id. Guard against
58            // re-prefixing an already-qualified id (e.g. a per-request model
59            // override that is already fully qualified) — the generic path
60            // applies this to the resolved request model unconditionally, so
61            // without the guard a qualified override would become an invalid
62            // `accounts/fireworks/models/accounts/fireworks/models/...` id.
63            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
105// ================================================================
106// Main Huggingface Client
107// ================================================================
108const 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    // Structured-output support varies by sub-provider; keep the
138    // pre-migration behavior of dropping `output_schema` with a warning.
139    const SUPPORTS_RESPONSE_FORMAT: bool = false;
140
141    type Response = crate::providers::openai::CompletionResponse;
142
143    // Chat completions live under the router's `/v1` while verification,
144    // transcription, and image generation use root-relative paths, so the
145    // prefix cannot live in the client base URL.
146    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        // Some sub-providers (Fireworks) address models through a qualified
155        // identifier in the request body.
156        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        // A bare id is qualified once...
226        assert_eq!(
227            SubProvider::Fireworks.model_identifier("deepseek-v3"),
228            "accounts/fireworks/models/deepseek-v3"
229        );
230        // ...and an already-qualified id (e.g. a per-request model override)
231        // is left untouched rather than double-prefixed.
232        assert_eq!(
233            SubProvider::Fireworks.model_identifier("accounts/fireworks/models/deepseek-v3"),
234            "accounts/fireworks/models/deepseek-v3"
235        );
236        // Other sub-providers pass the id through verbatim.
237        assert_eq!(
238            SubProvider::HFInference.model_identifier("meta-llama/Llama-3.1-8B"),
239            "meta-llama/Llama-3.1-8B"
240        );
241    }
242}