use crate::error::LlmError;
use reqwest::header::{
AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT,
};
use std::collections::HashMap;
pub struct HttpHeaderBuilder {
headers: HeaderMap,
}
impl HttpHeaderBuilder {
pub fn new() -> Self {
Self {
headers: HeaderMap::new(),
}
}
pub fn with_bearer_auth(mut self, token: &str) -> Result<Self, LlmError> {
let auth_value = format!("Bearer {token}");
self.headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid API key format: {e}"))
})?,
);
Ok(self)
}
pub fn with_custom_auth(mut self, header_name: &str, value: &str) -> Result<Self, LlmError> {
let header_name = HeaderName::from_bytes(header_name.as_bytes()).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid header name '{header_name}': {e}"))
})?;
self.headers.insert(
header_name,
HeaderValue::from_str(value)
.map_err(|e| LlmError::ConfigurationError(format!("Invalid header value: {e}")))?,
);
Ok(self)
}
pub fn with_json_content_type(mut self) -> Self {
self.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
self
}
pub fn with_user_agent(mut self, user_agent: &str) -> Result<Self, LlmError> {
self.headers.insert(
USER_AGENT,
HeaderValue::from_str(user_agent)
.map_err(|e| LlmError::ConfigurationError(format!("Invalid user agent: {e}")))?,
);
Ok(self)
}
pub fn with_header(mut self, name: &str, value: &str) -> Result<Self, LlmError> {
let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid header name '{name}': {e}"))
})?;
self.headers.insert(
header_name,
HeaderValue::from_str(value).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid header value '{value}': {e}"))
})?,
);
Ok(self)
}
pub fn with_custom_headers(
mut self,
custom_headers: &HashMap<String, String>,
) -> Result<Self, LlmError> {
for (key, value) in custom_headers {
let header_name = HeaderName::from_bytes(key.as_bytes()).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid header name '{key}': {e}"))
})?;
self.headers.insert(
header_name,
HeaderValue::from_str(value).map_err(|e| {
LlmError::ConfigurationError(format!("Invalid header value '{value}': {e}"))
})?,
);
}
Ok(self)
}
pub fn build(self) -> HeaderMap {
self.headers
}
}
impl Default for HttpHeaderBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct ProviderHeaders;
impl ProviderHeaders {
pub fn openai(
api_key: &str,
organization: Option<&str>,
project: Option<&str>,
custom_headers: &HashMap<String, String>,
) -> Result<HeaderMap, LlmError> {
let mut builder = HttpHeaderBuilder::new()
.with_bearer_auth(api_key)?
.with_json_content_type();
if let Some(org) = organization {
builder = builder.with_header("OpenAI-Organization", org)?;
}
if let Some(proj) = project {
builder = builder.with_header("OpenAI-Project", proj)?;
}
builder = builder.with_custom_headers(custom_headers)?;
Ok(builder.build())
}
pub fn anthropic(
api_key: &str,
custom_headers: &HashMap<String, String>,
) -> Result<HeaderMap, LlmError> {
let mut builder = HttpHeaderBuilder::new()
.with_custom_auth("x-api-key", api_key)?
.with_json_content_type()
.with_header("anthropic-version", "2023-06-01")?;
if let Some(beta_features) = custom_headers.get("anthropic-beta") {
builder = builder.with_header("anthropic-beta", beta_features)?;
}
let filtered_headers: HashMap<String, String> = custom_headers
.iter()
.filter(|(k, _)| k.as_str() != "anthropic-beta")
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
builder = builder.with_custom_headers(&filtered_headers)?;
Ok(builder.build())
}
pub fn groq(
api_key: &str,
custom_headers: &HashMap<String, String>,
) -> Result<HeaderMap, LlmError> {
let builder = HttpHeaderBuilder::new()
.with_bearer_auth(api_key)?
.with_json_content_type()
.with_user_agent("siumai/0.1.0 (groq-provider)")?
.with_custom_headers(custom_headers)?;
Ok(builder.build())
}
pub fn xai(
api_key: &str,
custom_headers: &HashMap<String, String>,
) -> Result<HeaderMap, LlmError> {
let builder = HttpHeaderBuilder::new()
.with_bearer_auth(api_key)?
.with_json_content_type()
.with_custom_headers(custom_headers)?;
Ok(builder.build())
}
pub fn ollama(custom_headers: &HashMap<String, String>) -> Result<HeaderMap, LlmError> {
let version = env!("CARGO_PKG_VERSION");
let builder = HttpHeaderBuilder::new()
.with_json_content_type()
.with_user_agent(&format!("siumai/{version}"))?
.with_custom_headers(custom_headers)?;
Ok(builder.build())
}
pub fn gemini(
api_key: &str,
custom_headers: &HashMap<String, String>,
) -> Result<HeaderMap, LlmError> {
let builder = HttpHeaderBuilder::new()
.with_custom_auth("x-goog-api-key", api_key)?
.with_json_content_type()
.with_custom_headers(custom_headers)?;
Ok(builder.build())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_builder() {
let headers = HttpHeaderBuilder::new()
.with_bearer_auth("test-token")
.unwrap()
.with_json_content_type()
.with_user_agent("test-agent")
.unwrap()
.build();
assert_eq!(headers.get(AUTHORIZATION).unwrap(), "Bearer test-token");
assert_eq!(headers.get(CONTENT_TYPE).unwrap(), "application/json");
assert_eq!(headers.get(USER_AGENT).unwrap(), "test-agent");
}
#[test]
fn test_openai_headers() {
let custom_headers = HashMap::new();
let headers =
ProviderHeaders::openai("test-key", Some("org"), Some("proj"), &custom_headers)
.unwrap();
assert_eq!(headers.get(AUTHORIZATION).unwrap(), "Bearer test-key");
assert_eq!(headers.get("OpenAI-Organization").unwrap(), "org");
assert_eq!(headers.get("OpenAI-Project").unwrap(), "proj");
}
#[test]
fn test_anthropic_headers() {
let custom_headers = HashMap::new();
let headers = ProviderHeaders::anthropic("test-key", &custom_headers).unwrap();
assert_eq!(headers.get("x-api-key").unwrap(), "test-key");
assert_eq!(headers.get("anthropic-version").unwrap(), "2023-06-01");
}
}