use super::openai::{AssistantContent, send_compatible_streaming_request};
use crate::client::{self, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder};
use crate::client::{BearerAuth, ProviderClient};
use crate::http_client::{self, HttpClientExt};
use crate::streaming::StreamingCompletionResponse;
use crate::providers::openai;
use crate::{
OneOrMany,
completion::{self, CompletionError, CompletionRequest},
json_utils,
providers::openai::Message,
};
use serde::{Deserialize, Serialize};
const HYPERBOLIC_API_BASE_URL: &str = "https://api.hyperbolic.xyz";
#[derive(Debug, Default, Clone, Copy)]
pub struct HyperbolicExt;
#[derive(Debug, Default, Clone, Copy)]
pub struct HyperbolicBuilder;
type HyperbolicApiKey = BearerAuth;
impl Provider for HyperbolicExt {
type Builder = HyperbolicBuilder;
const VERIFY_PATH: &'static str = "/models";
}
impl<H> Capabilities<H> for HyperbolicExt {
type Completion = Capable<CompletionModel<H>>;
type Embeddings = Nothing;
type Transcription = Nothing;
type ModelListing = Nothing;
#[cfg(feature = "image")]
type ImageGeneration = Capable<ImageGenerationModel<H>>;
#[cfg(feature = "audio")]
type AudioGeneration = Capable<AudioGenerationModel<H>>;
}
impl DebugExt for HyperbolicExt {}
impl ProviderBuilder for HyperbolicBuilder {
type Extension<H>
= HyperbolicExt
where
H: HttpClientExt;
type ApiKey = HyperbolicApiKey;
const BASE_URL: &'static str = HYPERBOLIC_API_BASE_URL;
fn build<H>(
_builder: &crate::client::ClientBuilder<Self, Self::ApiKey, H>,
) -> http_client::Result<Self::Extension<H>>
where
H: HttpClientExt,
{
Ok(HyperbolicExt)
}
}
pub type Client<H = reqwest::Client> = client::Client<HyperbolicExt, H>;
pub type ClientBuilder<H = reqwest::Client> = client::ClientBuilder<HyperbolicBuilder, String, H>;
impl ProviderClient for Client {
type Input = HyperbolicApiKey;
fn from_env() -> Self {
let api_key = std::env::var("HYPERBOLIC_API_KEY").expect("HYPERBOLIC_API_KEY not set");
Self::new(&api_key).unwrap()
}
fn from_val(input: Self::Input) -> Self {
Self::new(input).unwrap()
}
}
#[derive(Debug, Deserialize)]
struct ApiErrorResponse {
message: String,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ApiResponse<T> {
Ok(T),
Err(ApiErrorResponse),
}
#[derive(Debug, Deserialize)]
pub struct EmbeddingData {
pub object: String,
pub embedding: Vec<f64>,
pub index: usize,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Usage {
pub prompt_tokens: usize,
pub total_tokens: usize,
}
impl std::fmt::Display for Usage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Prompt tokens: {} Total tokens: {}",
self.prompt_tokens, self.total_tokens
)
}
}
pub const LLAMA_3_1_8B: &str = "meta-llama/Meta-Llama-3.1-8B-Instruct";
pub const LLAMA_3_3_70B: &str = "meta-llama/Llama-3.3-70B-Instruct";
pub const LLAMA_3_1_70B: &str = "meta-llama/Meta-Llama-3.1-70B-Instruct";
pub const LLAMA_3_70B: &str = "meta-llama/Meta-Llama-3-70B-Instruct";
pub const HERMES_3_70B: &str = "NousResearch/Hermes-3-Llama-3.1-70b";
pub const DEEPSEEK_2_5: &str = "deepseek-ai/DeepSeek-V2.5";
pub const QWEN_2_5_72B: &str = "Qwen/Qwen2.5-72B-Instruct";
pub const LLAMA_3_2_3B: &str = "meta-llama/Llama-3.2-3B-Instruct";
pub const QWEN_2_5_CODER_32B: &str = "Qwen/Qwen2.5-Coder-32B-Instruct";
pub const QWEN_QWQ_PREVIEW_32B: &str = "Qwen/QwQ-32B-Preview";
pub const DEEPSEEK_R1_ZERO: &str = "deepseek-ai/DeepSeek-R1-Zero";
pub const DEEPSEEK_R1: &str = "deepseek-ai/DeepSeek-R1";
#[derive(Debug, Deserialize, Serialize)]
pub struct CompletionResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Option<Usage>,
}
impl From<ApiErrorResponse> for CompletionError {
fn from(err: ApiErrorResponse) -> Self {
CompletionError::ProviderError(err.message)
}
}
impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
type Error = CompletionError;
fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
let choice = response.choices.first().ok_or_else(|| {
CompletionError::ResponseError("Response contained no choices".to_owned())
})?;
let content = match &choice.message {
Message::Assistant {
content,
tool_calls,
..
} => {
let mut content = content
.iter()
.map(|c| match c {
AssistantContent::Text { text } => completion::AssistantContent::text(text),
AssistantContent::Refusal { refusal } => {
completion::AssistantContent::text(refusal)
}
})
.collect::<Vec<_>>();
content.extend(
tool_calls
.iter()
.map(|call| {
completion::AssistantContent::tool_call(
&call.id,
&call.function.name,
call.function.arguments.clone(),
)
})
.collect::<Vec<_>>(),
);
Ok(content)
}
_ => Err(CompletionError::ResponseError(
"Response did not contain a valid message or tool call".into(),
)),
}?;
let choice = OneOrMany::many(content).map_err(|_| {
CompletionError::ResponseError(
"Response contained no message or tool call (empty)".to_owned(),
)
})?;
let usage = response
.usage
.as_ref()
.map(|usage| completion::Usage {
input_tokens: usage.prompt_tokens as u64,
output_tokens: (usage.total_tokens - usage.prompt_tokens) as u64,
total_tokens: usage.total_tokens as u64,
cached_input_tokens: 0,
cache_creation_input_tokens: 0,
})
.unwrap_or_default();
Ok(completion::CompletionResponse {
choice,
usage,
raw_response: response,
message_id: None,
})
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Choice {
pub index: usize,
pub message: Message,
pub finish_reason: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub(super) struct HyperbolicCompletionRequest {
model: String,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f64>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub additional_params: Option<serde_json::Value>,
}
impl TryFrom<(&str, CompletionRequest)> for HyperbolicCompletionRequest {
type Error = CompletionError;
fn try_from((model, req): (&str, CompletionRequest)) -> Result<Self, Self::Error> {
if req.output_schema.is_some() {
tracing::warn!("Structured outputs currently not supported for Hyperbolic");
}
let model = req.model.clone().unwrap_or_else(|| model.to_string());
if req.tool_choice.is_some() {
tracing::warn!("WARNING: `tool_choice` not supported on Hyperbolic");
}
if !req.tools.is_empty() {
tracing::warn!("WARNING: `tools` not supported on Hyperbolic");
}
let mut full_history: Vec<Message> = match &req.preamble {
Some(preamble) => vec![Message::system(preamble)],
None => vec![],
};
if let Some(docs) = req.normalized_documents() {
let docs: Vec<Message> = docs.try_into()?;
full_history.extend(docs);
}
let chat_history: Vec<Message> = req
.chat_history
.clone()
.into_iter()
.map(|message| message.try_into())
.collect::<Result<Vec<Vec<Message>>, _>>()?
.into_iter()
.flatten()
.collect();
full_history.extend(chat_history);
Ok(Self {
model: model.to_string(),
messages: full_history,
temperature: req.temperature,
additional_params: req.additional_params,
})
}
}
#[derive(Clone)]
pub struct CompletionModel<T = reqwest::Client> {
client: Client<T>,
pub model: String,
}
impl<T> CompletionModel<T> {
pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
Self {
client,
model: model.into(),
}
}
pub fn with_model(client: Client<T>, model: &str) -> Self {
Self {
client,
model: model.into(),
}
}
}
impl<T> completion::CompletionModel for CompletionModel<T>
where
T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
{
type Response = CompletionResponse;
type StreamingResponse = openai::StreamingCompletionResponse;
type Client = Client<T>;
fn make(client: &Self::Client, model: impl Into<String>) -> Self {
Self::new(client.clone(), model)
}
async fn completion(
&self,
completion_request: CompletionRequest,
) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
let span = if tracing::Span::current().is_disabled() {
info_span!(
target: "rig::completions",
"chat",
gen_ai.operation.name = "chat",
gen_ai.provider.name = "hyperbolic",
gen_ai.request.model = self.model,
gen_ai.system_instructions = tracing::field::Empty,
gen_ai.response.id = tracing::field::Empty,
gen_ai.response.model = tracing::field::Empty,
gen_ai.usage.output_tokens = tracing::field::Empty,
gen_ai.usage.input_tokens = tracing::field::Empty,
gen_ai.usage.cached_tokens = tracing::field::Empty,
)
} else {
tracing::Span::current()
};
span.record("gen_ai.system_instructions", &completion_request.preamble);
let request =
HyperbolicCompletionRequest::try_from((self.model.as_ref(), completion_request))?;
if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace!(target: "rig::completions",
"Hyperbolic completion request: {}",
serde_json::to_string_pretty(&request)?
);
}
let body = serde_json::to_vec(&request)?;
let req = self
.client
.post("/v1/chat/completions")?
.body(body)
.map_err(http_client::Error::from)?;
let async_block = async move {
let response = self.client.send::<_, bytes::Bytes>(req).await?;
let status = response.status();
let response_body = response.into_body().into_future().await?.to_vec();
if status.is_success() {
match serde_json::from_slice::<ApiResponse<CompletionResponse>>(&response_body)? {
ApiResponse::Ok(response) => {
if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace!(target: "rig::completions",
"Hyperbolic completion response: {}",
serde_json::to_string_pretty(&response)?
);
}
response.try_into()
}
ApiResponse::Err(err) => Err(CompletionError::ProviderError(err.message)),
}
} else {
Err(CompletionError::ProviderError(
String::from_utf8_lossy(&response_body).to_string(),
))
}
};
async_block.instrument(span).await
}
async fn stream(
&self,
completion_request: CompletionRequest,
) -> Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError> {
let span = if tracing::Span::current().is_disabled() {
info_span!(
target: "rig::completions",
"chat_streaming",
gen_ai.operation.name = "chat_streaming",
gen_ai.provider.name = "hyperbolic",
gen_ai.request.model = self.model,
gen_ai.system_instructions = tracing::field::Empty,
gen_ai.response.id = tracing::field::Empty,
gen_ai.response.model = tracing::field::Empty,
gen_ai.usage.output_tokens = tracing::field::Empty,
gen_ai.usage.input_tokens = tracing::field::Empty,
gen_ai.usage.cached_tokens = tracing::field::Empty,
)
} else {
tracing::Span::current()
};
span.record("gen_ai.system_instructions", &completion_request.preamble);
let mut request =
HyperbolicCompletionRequest::try_from((self.model.as_ref(), completion_request))?;
let params = json_utils::merge(
request.additional_params.unwrap_or(serde_json::json!({})),
serde_json::json!({"stream": true, "stream_options": {"include_usage": true} }),
);
request.additional_params = Some(params);
if tracing::enabled!(tracing::Level::TRACE) {
tracing::trace!(target: "rig::completions",
"Hyperbolic streaming completion request: {}",
serde_json::to_string_pretty(&request)?
);
}
let body = serde_json::to_vec(&request)?;
let req = self
.client
.post("/v1/chat/completions")?
.body(body)
.map_err(http_client::Error::from)?;
send_compatible_streaming_request(self.client.clone(), req)
.instrument(span)
.await
}
}
#[cfg(feature = "image")]
pub use image_generation::*;
#[cfg(feature = "image")]
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
mod image_generation {
use super::{ApiResponse, Client};
use crate::http_client::HttpClientExt;
use crate::image_generation;
use crate::image_generation::{ImageGenerationError, ImageGenerationRequest};
use crate::json_utils::merge_inplace;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde::Deserialize;
use serde_json::json;
pub const SDXL1_0_BASE: &str = "SDXL1.0-base";
pub const SD2: &str = "SD2";
pub const SD1_5: &str = "SD1.5";
pub const SSD: &str = "SSD";
pub const SDXL_TURBO: &str = "SDXL-turbo";
pub const SDXL_CONTROLNET: &str = "SDXL-ControlNet";
pub const SD1_5_CONTROLNET: &str = "SD1.5-ControlNet";
#[derive(Clone)]
pub struct ImageGenerationModel<T> {
client: Client<T>,
pub model: String,
}
impl<T> ImageGenerationModel<T> {
pub(crate) fn new(client: Client<T>, model: impl Into<String>) -> Self {
Self {
client,
model: model.into(),
}
}
pub fn with_model(client: Client<T>, model: &str) -> Self {
Self {
client,
model: model.into(),
}
}
}
#[derive(Clone, Deserialize)]
pub struct Image {
image: String,
}
#[derive(Clone, Deserialize)]
pub struct ImageGenerationResponse {
images: Vec<Image>,
}
impl TryFrom<ImageGenerationResponse>
for image_generation::ImageGenerationResponse<ImageGenerationResponse>
{
type Error = ImageGenerationError;
fn try_from(value: ImageGenerationResponse) -> Result<Self, Self::Error> {
let data = BASE64_STANDARD
.decode(&value.images[0].image)
.expect("Could not decode image.");
Ok(Self {
image: data,
response: value,
})
}
}
impl<T> image_generation::ImageGenerationModel for ImageGenerationModel<T>
where
T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
{
type Response = ImageGenerationResponse;
type Client = Client<T>;
fn make(client: &Self::Client, model: impl Into<String>) -> Self {
Self::new(client.clone(), model)
}
async fn image_generation(
&self,
generation_request: ImageGenerationRequest,
) -> Result<image_generation::ImageGenerationResponse<Self::Response>, ImageGenerationError>
{
let mut request = json!({
"model_name": self.model,
"prompt": generation_request.prompt,
"height": generation_request.height,
"width": generation_request.width,
});
if let Some(params) = generation_request.additional_params {
merge_inplace(&mut request, params);
}
let body = serde_json::to_vec(&request)?;
let request = self
.client
.post("/v1/image/generation")?
.header("Content-Type", "application/json")
.body(body)
.map_err(|e| ImageGenerationError::HttpError(e.into()))?;
let response = self.client.send::<_, bytes::Bytes>(request).await?;
let status = response.status();
let response_body = response.into_body().into_future().await?.to_vec();
if !status.is_success() {
return Err(ImageGenerationError::ProviderError(format!(
"{status}: {}",
String::from_utf8_lossy(&response_body)
)));
}
match serde_json::from_slice::<ApiResponse<ImageGenerationResponse>>(&response_body)? {
ApiResponse::Ok(response) => response.try_into(),
ApiResponse::Err(err) => Err(ImageGenerationError::ResponseError(err.message)),
}
}
}
}
#[cfg(feature = "audio")]
pub use audio_generation::*;
use tracing::{Instrument, info_span};
#[cfg(feature = "audio")]
#[cfg_attr(docsrs, doc(cfg(feature = "image")))]
mod audio_generation {
use super::{ApiResponse, Client};
use crate::audio_generation;
use crate::audio_generation::{AudioGenerationError, AudioGenerationRequest};
use crate::http_client::{self, HttpClientExt};
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use bytes::Bytes;
use serde::Deserialize;
use serde_json::json;
#[derive(Clone)]
pub struct AudioGenerationModel<T> {
client: Client<T>,
pub language: String,
}
#[derive(Clone, Deserialize)]
pub struct AudioGenerationResponse {
audio: String,
}
impl TryFrom<AudioGenerationResponse>
for audio_generation::AudioGenerationResponse<AudioGenerationResponse>
{
type Error = AudioGenerationError;
fn try_from(value: AudioGenerationResponse) -> Result<Self, Self::Error> {
let data = BASE64_STANDARD
.decode(&value.audio)
.expect("Could not decode audio.");
Ok(Self {
audio: data,
response: value,
})
}
}
impl<T> audio_generation::AudioGenerationModel for AudioGenerationModel<T>
where
T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
{
type Response = AudioGenerationResponse;
type Client = Client<T>;
fn make(client: &Self::Client, language: impl Into<String>) -> Self {
Self {
client: client.clone(),
language: language.into(),
}
}
async fn audio_generation(
&self,
request: AudioGenerationRequest,
) -> Result<audio_generation::AudioGenerationResponse<Self::Response>, AudioGenerationError>
{
let request = json!({
"language": self.language,
"speaker": request.voice,
"text": request.text,
"speed": request.speed
});
let body = serde_json::to_vec(&request)?;
let req = self
.client
.post("/v1/audio/generation")?
.body(body)
.map_err(http_client::Error::from)?;
let response = self.client.send::<_, Bytes>(req).await?;
let status = response.status();
let response_body = response.into_body().into_future().await?.to_vec();
if !status.is_success() {
return Err(AudioGenerationError::ProviderError(format!(
"{status}: {}",
String::from_utf8_lossy(&response_body)
)));
}
match serde_json::from_slice::<ApiResponse<AudioGenerationResponse>>(&response_body)? {
ApiResponse::Ok(response) => response.try_into(),
ApiResponse::Err(err) => Err(AudioGenerationError::ProviderError(err.message)),
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_client_initialization() {
let _client =
crate::providers::hyperbolic::Client::new("dummy-key").expect("Client::new() failed");
let _client_from_builder = crate::providers::hyperbolic::Client::builder()
.api_key("dummy-key")
.build()
.expect("Client::builder() failed");
}
}