1use crate::client::BearerAuth;
16use crate::client::{self, DebugExt, Provider};
17
18const HYPERBOLIC_API_BASE_URL: &str = "https://api.hyperbolic.xyz";
22
23#[derive(Debug, Default, Clone, Copy)]
24pub struct HyperbolicExt;
25#[derive(Debug, Default, Clone, Copy)]
26pub struct HyperbolicBuilder;
27
28type HyperbolicApiKey = BearerAuth;
29
30impl Provider for HyperbolicExt {
31 type Builder = HyperbolicBuilder;
32
33 const VERIFY_PATH: &'static str = "/models";
34}
35
36client::impl_capabilities!(
37 HyperbolicExt,
38 completion = CompletionModel<H>,
39 image_generation = ImageGenerationModel<H>,
40 audio_generation = AudioGenerationModel<H>,
41);
42
43impl DebugExt for HyperbolicExt {}
44
45impl crate::providers::openai::completion::OpenAICompatibleProvider for HyperbolicExt {
46 const PROVIDER_NAME: &'static str = "hyperbolic";
47
48 const SUPPORTS_RESPONSE_FORMAT: bool = false;
51
52 const SUPPORTS_TOOLS: bool = false;
55
56 type StreamingUsage = crate::providers::openai::Usage;
57
58 type Response = crate::providers::openai::CompletionResponse;
59
60 fn finalize_request_body(
61 &self,
62 body: &mut serde_json::Value,
63 ) -> Result<(), crate::completion::CompletionError> {
64 if let Some(messages) = body
67 .get_mut("messages")
68 .and_then(serde_json::Value::as_array_mut)
69 {
70 crate::providers::openai::completion::sanitize_plain_text_history(
71 messages, None, false, false,
72 );
73 }
74
75 Ok(())
76 }
77
78 fn completion_path(&self, _model: &str) -> String {
81 "/v1/chat/completions".to_string()
82 }
83}
84
85client::impl_default_provider_builder!(
86 HyperbolicBuilder => HyperbolicExt,
87 api_key = HyperbolicApiKey,
88 base_url = HYPERBOLIC_API_BASE_URL,
89);
90
91pub type Client<H = reqwest::Client> = client::Client<HyperbolicExt, H>;
92pub type ClientBuilder<H = crate::markers::Missing> =
93 client::ClientBuilder<HyperbolicBuilder, HyperbolicApiKey, H>;
94
95client::impl_provider_client!(
96 Client,
97 input = HyperbolicApiKey,
98 api_key_env = "HYPERBOLIC_API_KEY",
99);
100
101#[cfg(feature = "audio")]
102use crate::providers::openai::client::ApiResponse;
103
104pub const LLAMA_3_1_8B: &str = "meta-llama/Meta-Llama-3.1-8B-Instruct";
110pub const LLAMA_3_3_70B: &str = "meta-llama/Llama-3.3-70B-Instruct";
112pub const LLAMA_3_1_70B: &str = "meta-llama/Meta-Llama-3.1-70B-Instruct";
114pub const LLAMA_3_70B: &str = "meta-llama/Meta-Llama-3-70B-Instruct";
116pub const HERMES_3_70B: &str = "NousResearch/Hermes-3-Llama-3.1-70b";
118pub const DEEPSEEK_2_5: &str = "deepseek-ai/DeepSeek-V2.5";
120pub const QWEN_2_5_72B: &str = "Qwen/Qwen2.5-72B-Instruct";
122pub const LLAMA_3_2_3B: &str = "meta-llama/Llama-3.2-3B-Instruct";
124pub const QWEN_2_5_CODER_32B: &str = "Qwen/Qwen2.5-Coder-32B-Instruct";
126pub const QWEN_QWQ_PREVIEW_32B: &str = "Qwen/QwQ-32B-Preview";
128pub const DEEPSEEK_R1_ZERO: &str = "deepseek-ai/DeepSeek-R1-Zero";
130pub const DEEPSEEK_R1: &str = "deepseek-ai/DeepSeek-R1";
132
133pub type CompletionModel<H = reqwest::Client> =
135 crate::providers::openai::completion::GenericCompletionModel<HyperbolicExt, H>;
136
137pub type CompletionResponse = crate::providers::openai::CompletionResponse;
139
140#[cfg(feature = "image")]
145pub use image_generation::*;
146
147#[cfg(feature = "image")]
148#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
149mod image_generation {
150 use super::HyperbolicExt;
151 use crate::image_generation;
152 use crate::image_generation::{ImageGenerationError, ImageGenerationRequest};
153 use crate::json_utils::merge_inplace;
154 use crate::providers::internal::image_generation::{
155 GenericImageGenerationModel, JsonImageGenerationProvider, decode_base64_image,
156 };
157 use serde::Deserialize;
158 use serde_json::json;
159
160 pub const SDXL1_0_BASE: &str = "SDXL1.0-base";
161 pub const SD2: &str = "SD2";
162 pub const SD1_5: &str = "SD1.5";
163 pub const SSD: &str = "SSD";
164 pub const SDXL_TURBO: &str = "SDXL-turbo";
165 pub const SDXL_CONTROLNET: &str = "SDXL-ControlNet";
166 pub const SD1_5_CONTROLNET: &str = "SD1.5-ControlNet";
167
168 pub type ImageGenerationModel<T> = GenericImageGenerationModel<HyperbolicExt, T>;
170
171 #[derive(Clone, Deserialize)]
172 pub struct Image {
173 image: String,
174 }
175
176 #[derive(Clone, Deserialize)]
177 pub struct ImageGenerationResponse {
178 images: Vec<Image>,
179 }
180
181 impl TryFrom<ImageGenerationResponse>
182 for image_generation::ImageGenerationResponse<ImageGenerationResponse>
183 {
184 type Error = ImageGenerationError;
185
186 fn try_from(value: ImageGenerationResponse) -> Result<Self, Self::Error> {
187 decode_base64_image(
188 value,
189 |response| response.images.first().map(|image| image.image.as_str()),
190 "missing image data",
191 None,
192 )
193 }
194 }
195
196 impl JsonImageGenerationProvider for HyperbolicExt {
197 const IMAGE_GENERATION_PATH: &'static str = "/v1/image/generation";
198 type Response = ImageGenerationResponse;
199
200 fn image_generation_request_body(
201 model: &str,
202 generation_request: ImageGenerationRequest,
203 ) -> Result<serde_json::Value, ImageGenerationError> {
204 let mut request = json!({
205 "model_name": model,
206 "prompt": generation_request.prompt,
207 "height": generation_request.height,
208 "width": generation_request.width,
209 });
210
211 if let Some(params) = generation_request.additional_params {
212 merge_inplace(&mut request, params);
213 }
214
215 Ok(request)
216 }
217 }
218}
219
220#[cfg(feature = "audio")]
224pub use audio_generation::*;
225
226#[cfg(feature = "audio")]
227#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
228mod audio_generation {
229 use super::{ApiResponse, Client};
230 use crate::audio_generation;
231 use crate::audio_generation::{AudioGenerationError, AudioGenerationRequest};
232 use crate::http_client::{self, HttpClientExt};
233 use base64::Engine;
234 use base64::prelude::BASE64_STANDARD;
235 use bytes::Bytes;
236 use serde::Deserialize;
237 use serde_json::json;
238
239 #[derive(Clone)]
240 pub struct AudioGenerationModel<T> {
241 client: Client<T>,
242 pub language: String,
243 }
244
245 #[derive(Clone, Deserialize)]
246 pub struct AudioGenerationResponse {
247 audio: String,
248 }
249
250 impl TryFrom<AudioGenerationResponse>
251 for audio_generation::AudioGenerationResponse<AudioGenerationResponse>
252 {
253 type Error = AudioGenerationError;
254
255 fn try_from(value: AudioGenerationResponse) -> Result<Self, Self::Error> {
256 let data = BASE64_STANDARD
257 .decode(&value.audio)
258 .map_err(|err| AudioGenerationError::ResponseError(err.to_string()))?;
259
260 Ok(Self {
261 audio: data,
262 response: value,
263 })
264 }
265 }
266
267 impl<T> audio_generation::AudioGenerationModel for AudioGenerationModel<T>
268 where
269 T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
270 {
271 type Response = AudioGenerationResponse;
272 type Client = Client<T>;
273
274 fn make(client: &Self::Client, language: impl Into<String>) -> Self {
275 Self {
276 client: client.clone(),
277 language: language.into(),
278 }
279 }
280
281 async fn audio_generation(
282 &self,
283 request: AudioGenerationRequest,
284 ) -> Result<audio_generation::AudioGenerationResponse<Self::Response>, AudioGenerationError>
285 {
286 let request = json!({
287 "language": self.language,
288 "speaker": request.voice,
289 "text": request.text,
290 "speed": request.speed
291 });
292
293 let body = serde_json::to_vec(&request)?;
294
295 let req = self
296 .client
297 .post("/v1/audio/generation")?
298 .body(body)
299 .map_err(http_client::Error::from)?;
300
301 let response = self.client.send::<_, Bytes>(req).await?;
302 let status = response.status();
303 let response_body = response.into_body().into_future().await?.to_vec();
304
305 if !status.is_success() {
306 return Err(AudioGenerationError::from_http_response(
307 status,
308 String::from_utf8_lossy(&response_body),
309 ));
310 }
311
312 match serde_json::from_slice::<ApiResponse<AudioGenerationResponse>>(&response_body)? {
313 ApiResponse::Ok(response) => response.try_into(),
314 ApiResponse::Err(err) => {
315 tracing::warn!(message = %err.message, "provider returned an error response");
316 Err(AudioGenerationError::from_http_response(
317 status,
318 String::from_utf8_lossy(&response_body),
319 ))
320 }
321 }
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 #[test]
329 fn hyperbolic_prepare_request_drops_tools_and_tool_choice() {
330 use crate::providers::openai::completion::{
331 CompletionRequest as OpenAICompletionRequest, OpenAICompatibleProvider,
332 OpenAIRequestParams,
333 };
334
335 let request = crate::completion::CompletionRequestBuilder::new(
336 crate::test_utils::MockCompletionModel::default(),
337 "hello",
338 )
339 .tool(crate::completion::ToolDefinition {
340 name: "lookup".to_string(),
341 description: "Lookup".to_string(),
342 parameters: serde_json::json!({"type":"object","properties":{},"required":[]}),
343 })
344 .tool_choice(crate::message::ToolChoice::Required)
345 .output_schema(schemars::schema_for!(serde_json::Value))
346 .build();
347
348 let mut request = OpenAICompletionRequest::try_from(OpenAIRequestParams {
349 model: "meta-llama/Meta-Llama-3.1-8B-Instruct".to_string(),
350 request,
351 strict_tools: false,
352 tool_result_array_content: false,
353 supports_response_format: super::HyperbolicExt::SUPPORTS_RESPONSE_FORMAT,
354 supports_tools: false,
355 })
356 .expect("request should convert");
357 super::HyperbolicExt
358 .prepare_request(&mut request)
359 .expect("prepare_request should succeed");
360
361 let body = serde_json::to_value(request).expect("request should serialize");
362 assert!(body.get("tools").is_none());
363 assert!(body.get("tool_choice").is_none());
364 assert!(body.get("response_format").is_none());
365 }
366
367 #[test]
368 fn test_client_initialization() {
369 let _client =
370 crate::providers::hyperbolic::Client::new("dummy-key").expect("Client::new() failed");
371 let builder: crate::providers::hyperbolic::ClientBuilder =
372 crate::providers::hyperbolic::Client::builder().api_key("dummy-key");
373 let _client_from_builder = builder.build().expect("Client::builder() failed");
374 }
375
376 #[tokio::test]
377 async fn completion_non_success_preserves_status_and_body() {
378 use crate::client::CompletionClient;
379 use crate::completion::{CompletionError, CompletionModel};
380 use crate::test_utils::RecordingHttpClient;
381
382 let body = r#"{"error":{"message":"boom"}}"#;
383 let http_client =
384 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
385 let client = super::Client::builder()
386 .api_key("test-key")
387 .http_client(http_client)
388 .build()
389 .expect("build client");
390 let model = client.completion_model(super::LLAMA_3_1_8B);
391 let request = model.completion_request("hello").build();
392
393 let error = model
394 .completion(request)
395 .await
396 .expect_err("completion should fail with non-success status");
397
398 assert!(matches!(error, CompletionError::HttpError(_)));
399 assert_eq!(
400 error.provider_response_status(),
401 Some(http::StatusCode::SERVICE_UNAVAILABLE)
402 );
403 assert_eq!(error.provider_response_body(), Some(body));
404 }
405
406 #[tokio::test]
407 async fn completion_2xx_error_envelope_preserves_status_and_body() {
408 use crate::client::CompletionClient;
409 use crate::completion::{CompletionError, CompletionModel};
410 use crate::test_utils::RecordingHttpClient;
411
412 let body = r#"{"message":"boom"}"#;
413 let http_client = RecordingHttpClient::new(body); let client = super::Client::builder()
415 .api_key("test-key")
416 .http_client(http_client)
417 .build()
418 .expect("build client");
419 let model = client.completion_model(super::LLAMA_3_1_8B);
420 let request = model.completion_request("hello").build();
421
422 let error = model
423 .completion(request)
424 .await
425 .expect_err("completion should fail with provider error envelope");
426
427 match &error {
428 CompletionError::ProviderResponse(stored) => {
429 assert_eq!(stored.body, body);
430 assert_eq!(stored.status, Some(http::StatusCode::OK));
431 }
432 other => panic!("expected ProviderResponse, got {other:?}"),
433 }
434 }
435
436 #[cfg(feature = "image")]
437 #[tokio::test]
438 async fn image_generation_non_success_preserves_status_and_body() {
439 use crate::client::image_generation::ImageGenerationClient;
440 use crate::image_generation::{
441 ImageGenerationError, ImageGenerationModel as _, ImageGenerationRequest,
442 };
443 use crate::test_utils::RecordingHttpClient;
444
445 let body = r#"{"error":{"message":"boom"}}"#;
446 let http_client =
447 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
448 let client = super::Client::builder()
449 .api_key("test-key")
450 .http_client(http_client)
451 .build()
452 .expect("build client");
453 let model = client.image_generation_model(super::SDXL1_0_BASE);
454
455 let request = ImageGenerationRequest {
456 prompt: "draw a cat".to_string(),
457 width: 256,
458 height: 256,
459 additional_params: None,
460 };
461
462 let error = model
463 .image_generation(request)
464 .await
465 .err()
466 .expect("image generation should fail with non-success status");
467
468 assert!(matches!(error, ImageGenerationError::HttpError(_)));
469 assert_eq!(
470 error.provider_response_status(),
471 Some(http::StatusCode::SERVICE_UNAVAILABLE)
472 );
473 assert_eq!(error.provider_response_body(), Some(body));
474 }
475
476 #[cfg(feature = "image")]
477 #[tokio::test]
478 async fn image_generation_2xx_error_envelope_preserves_status_and_body() {
479 use crate::client::image_generation::ImageGenerationClient;
480 use crate::image_generation::{
481 ImageGenerationError, ImageGenerationModel as _, ImageGenerationRequest,
482 };
483 use crate::test_utils::RecordingHttpClient;
484
485 let body = r#"{"message":"boom"}"#;
486 let http_client = RecordingHttpClient::new(body); let client = super::Client::builder()
488 .api_key("test-key")
489 .http_client(http_client)
490 .build()
491 .expect("build client");
492 let model = client.image_generation_model(super::SDXL1_0_BASE);
493
494 let request = ImageGenerationRequest {
495 prompt: "draw a cat".to_string(),
496 width: 256,
497 height: 256,
498 additional_params: None,
499 };
500
501 let error = model
502 .image_generation(request)
503 .await
504 .err()
505 .expect("image generation should fail with provider error envelope");
506
507 match &error {
508 ImageGenerationError::ProviderResponse(stored) => {
509 assert_eq!(stored.body, body);
510 assert_eq!(stored.status, Some(http::StatusCode::OK));
511 }
512 other => panic!("expected ProviderResponse, got {other:?}"),
513 }
514 }
515
516 #[cfg(feature = "audio")]
517 #[tokio::test]
518 async fn audio_generation_non_success_preserves_status_and_body() {
519 use crate::audio_generation::{
520 AudioGenerationError, AudioGenerationModel as _, AudioGenerationRequest,
521 };
522 use crate::client::audio_generation::AudioGenerationClient;
523 use crate::test_utils::RecordingHttpClient;
524
525 let body = r#"{"error":{"message":"boom"}}"#;
526 let http_client =
527 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
528 let client = super::Client::builder()
529 .api_key("test-key")
530 .http_client(http_client)
531 .build()
532 .expect("build client");
533 let model = client.audio_generation_model("EN");
534
535 let request = AudioGenerationRequest {
536 text: "hello".to_string(),
537 voice: "default".to_string(),
538 speed: 1.0,
539 additional_params: None,
540 };
541
542 let error = model
543 .audio_generation(request)
544 .await
545 .err()
546 .expect("audio generation should fail with non-success status");
547
548 assert!(matches!(error, AudioGenerationError::HttpError(_)));
549 assert_eq!(
550 error.provider_response_status(),
551 Some(http::StatusCode::SERVICE_UNAVAILABLE)
552 );
553 assert_eq!(error.provider_response_body(), Some(body));
554 }
555
556 #[cfg(feature = "audio")]
557 #[tokio::test]
558 async fn audio_generation_2xx_error_envelope_preserves_status_and_body() {
559 use crate::audio_generation::{
560 AudioGenerationError, AudioGenerationModel as _, AudioGenerationRequest,
561 };
562 use crate::client::audio_generation::AudioGenerationClient;
563 use crate::test_utils::RecordingHttpClient;
564
565 let body = r#"{"message":"boom"}"#;
566 let http_client = RecordingHttpClient::new(body); let client = super::Client::builder()
568 .api_key("test-key")
569 .http_client(http_client)
570 .build()
571 .expect("build client");
572 let model = client.audio_generation_model("EN");
573
574 let request = AudioGenerationRequest {
575 text: "hello".to_string(),
576 voice: "default".to_string(),
577 speed: 1.0,
578 additional_params: None,
579 };
580
581 let error = model
582 .audio_generation(request)
583 .await
584 .err()
585 .expect("audio generation should fail with provider error envelope");
586
587 match &error {
588 AudioGenerationError::ProviderResponse(stored) => {
589 assert_eq!(stored.body, body);
590 assert_eq!(stored.status, Some(http::StatusCode::OK));
591 }
592 other => panic!("expected ProviderResponse, got {other:?}"),
593 }
594 }
595}