use std::time::Duration;
use backon::ExponentialBuilder;
use reqwest::header::{HeaderName, HeaderValue, InvalidHeaderValue};
#[derive(Clone, Debug)]
pub struct ChromaRetryOptions {
pub max_retries: usize,
pub min_delay: Duration,
pub max_delay: Duration,
pub jitter: bool,
}
impl Default for ChromaRetryOptions {
fn default() -> Self {
Self {
max_retries: 3,
min_delay: Duration::from_millis(200),
max_delay: Duration::from_secs(5),
jitter: true,
}
}
}
impl From<ChromaRetryOptions> for ExponentialBuilder {
fn from(options: ChromaRetryOptions) -> Self {
let mut builder = ExponentialBuilder::new()
.with_max_times(options.max_retries)
.with_min_delay(options.min_delay)
.with_max_delay(options.max_delay);
if options.jitter {
builder = builder.with_jitter();
}
builder
}
}
#[derive(Debug, Clone)]
pub enum ChromaAuthMethod {
None,
HeaderAuth {
header: HeaderName,
value: HeaderValue,
},
}
impl ChromaAuthMethod {
pub fn cloud_api_key(key: &str) -> Result<Self, InvalidHeaderValue> {
let mut value = HeaderValue::from_str(key)?;
value.set_sensitive(true);
Ok(ChromaAuthMethod::HeaderAuth {
header: HeaderName::from_static("x-chroma-token"),
value,
})
}
pub(crate) fn chroma_cloud_api_key(&self) -> Option<&str> {
match self {
ChromaAuthMethod::HeaderAuth { header, value }
if header.as_str().eq_ignore_ascii_case("x-chroma-token") =>
{
value.to_str().ok()
}
ChromaAuthMethod::HeaderAuth { .. } | ChromaAuthMethod::None => None,
}
}
pub(crate) fn apply(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
match self {
ChromaAuthMethod::HeaderAuth { header, value } => {
request.header(header.clone(), value.clone())
}
ChromaAuthMethod::None => request,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ChromaHttpClientOptionsError {
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error("Invalid endpoint URL: {0}")]
InvalidEndpoint(String),
#[error("Missing required configuration: {0}")]
MissingConfiguration(String),
}
const DEFAULT_LOCAL_ENDPOINT: &str = "http://localhost:8000";
const DEFAULT_CLOUD_ENDPOINT: &str = "https://api.trychroma.com";
fn endpoint_from_env(default_endpoint: &str) -> Result<reqwest::Url, ChromaHttpClientOptionsError> {
let default_endpoint = default_endpoint.parse().expect("valid URL");
if let Ok(endpoint) = std::env::var("CHROMA_ENDPOINT") {
return endpoint
.parse::<reqwest::Url>()
.map_err(|err| ChromaHttpClientOptionsError::InvalidEndpoint(err.to_string()));
}
if let Ok(host) = std::env::var("CHROMA_HOST") {
return endpoint_from_host(&host, &default_endpoint);
}
Ok(default_endpoint)
}
fn endpoint_from_host(
host: &str,
default_endpoint: &reqwest::Url,
) -> Result<reqwest::Url, ChromaHttpClientOptionsError> {
let endpoint = if host.contains("://") {
host.to_string()
} else {
format!("{}://{}", default_endpoint.scheme(), host)
};
endpoint
.parse::<reqwest::Url>()
.map_err(|err| ChromaHttpClientOptionsError::InvalidEndpoint(err.to_string()))
}
#[derive(Debug, Clone)]
pub struct ChromaHttpClientOptions {
pub endpoint: reqwest::Url,
pub endpoints: Vec<reqwest::Url>,
pub auth_method: ChromaAuthMethod,
pub retry_options: ChromaRetryOptions,
pub tenant_id: Option<String>,
pub database_name: Option<String>,
}
impl Default for ChromaHttpClientOptions {
fn default() -> Self {
ChromaHttpClientOptions {
endpoint: DEFAULT_LOCAL_ENDPOINT.parse().expect("valid URL"),
endpoints: Vec::new(),
auth_method: ChromaAuthMethod::None,
retry_options: ChromaRetryOptions::default(),
tenant_id: None,
database_name: None,
}
}
}
impl ChromaHttpClientOptions {
pub fn from_env() -> Result<Self, ChromaHttpClientOptionsError> {
let endpoint = endpoint_from_env(DEFAULT_LOCAL_ENDPOINT)?;
let tenant_id = std::env::var("CHROMA_TENANT").unwrap_or("default_tenant".to_string());
let database_name =
std::env::var("CHROMA_DATABASE").unwrap_or("default_database".to_string());
Ok(ChromaHttpClientOptions {
database_name: Some(database_name),
tenant_id: Some(tenant_id),
endpoint,
..Default::default()
})
}
pub fn from_cloud_env() -> Result<Self, ChromaHttpClientOptionsError> {
let endpoint = endpoint_from_env(DEFAULT_CLOUD_ENDPOINT)?;
let api_key = std::env::var("CHROMA_API_KEY").map_err(|_| {
ChromaHttpClientOptionsError::MissingConfiguration("CHROMA_API_KEY".to_string())
})?;
let tenant_id = std::env::var("CHROMA_TENANT").ok();
let database_name = std::env::var("CHROMA_DATABASE").ok();
Ok(ChromaHttpClientOptions {
database_name,
tenant_id,
endpoint,
auth_method: ChromaAuthMethod::cloud_api_key(&api_key)?,
..Default::default()
})
}
pub fn cloud(
api_key: impl Into<String>,
database_name: impl Into<String>,
) -> Result<Self, ChromaHttpClientOptionsError> {
let api_key = api_key.into();
let database_name = database_name.into();
Ok(ChromaHttpClientOptions {
database_name: Some(database_name),
auth_method: ChromaAuthMethod::cloud_api_key(&api_key)?,
endpoint: DEFAULT_CLOUD_ENDPOINT.parse().expect("valid URL"),
..Default::default()
})
}
pub fn cloud_admin(api_key: impl Into<String>) -> Result<Self, ChromaHttpClientOptionsError> {
let api_key = api_key.into();
Ok(ChromaHttpClientOptions {
auth_method: ChromaAuthMethod::cloud_api_key(&api_key)?,
endpoint: DEFAULT_CLOUD_ENDPOINT.parse().expect("valid URL"),
endpoints: Vec::new(),
retry_options: ChromaRetryOptions::default(),
tenant_id: None,
database_name: None,
})
}
pub(crate) fn all_endpoints(&self) -> Vec<reqwest::Url> {
let mut endpoints = Vec::with_capacity(1 + self.endpoints.len());
endpoints.push(self.endpoint.clone());
for endpoint in &self.endpoints {
if !endpoints.iter().any(|existing| existing == endpoint) {
endpoints.push(endpoint.clone());
}
}
endpoints
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const ENV_KEYS: [&str; 5] = [
"CHROMA_API_KEY",
"CHROMA_DATABASE",
"CHROMA_ENDPOINT",
"CHROMA_HOST",
"CHROMA_TENANT",
];
struct EnvSnapshot {
values: Vec<(&'static str, Option<String>)>,
}
impl EnvSnapshot {
fn capture() -> Self {
Self {
values: ENV_KEYS
.into_iter()
.map(|key| (key, std::env::var(key).ok()))
.collect(),
}
}
fn clear() {
for key in ENV_KEYS {
std::env::remove_var(key);
}
}
}
impl Drop for EnvSnapshot {
fn drop(&mut self) {
for (key, value) in &self.values {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
}
fn with_chroma_env<T>(vars: &[(&'static str, &'static str)], test: impl FnOnce() -> T) -> T {
let _guard = ENV_LOCK.lock().unwrap();
let _snapshot = EnvSnapshot::capture();
EnvSnapshot::clear();
for (key, value) in vars {
std::env::set_var(key, value);
}
test()
}
#[test]
fn from_env_uses_chroma_host_when_endpoint_is_unset() {
with_chroma_env(&[("CHROMA_HOST", "http://example.com:9000")], || {
let options = ChromaHttpClientOptions::from_env().unwrap();
assert_eq!(options.endpoint.as_str(), "http://example.com:9000/");
});
}
#[test]
fn from_env_uses_bare_chroma_host_with_local_scheme() {
with_chroma_env(&[("CHROMA_HOST", "localhost:9000")], || {
let options = ChromaHttpClientOptions::from_env().unwrap();
assert_eq!(options.endpoint.as_str(), "http://localhost:9000/");
});
}
#[test]
fn from_env_prefers_chroma_endpoint_over_chroma_host() {
with_chroma_env(
&[
("CHROMA_ENDPOINT", "http://endpoint.example.com:9000"),
("CHROMA_HOST", "http://host.example.com:9000"),
],
|| {
let options = ChromaHttpClientOptions::from_env().unwrap();
assert_eq!(
options.endpoint.as_str(),
"http://endpoint.example.com:9000/"
);
},
);
}
#[test]
fn from_cloud_env_uses_chroma_host_when_endpoint_is_unset() {
with_chroma_env(
&[
("CHROMA_API_KEY", "test-key"),
("CHROMA_HOST", "https://cloud.example.com"),
],
|| {
let options = ChromaHttpClientOptions::from_cloud_env().unwrap();
assert_eq!(options.endpoint.as_str(), "https://cloud.example.com/");
},
);
}
#[test]
fn from_cloud_env_uses_bare_chroma_host_with_cloud_scheme() {
with_chroma_env(
&[
("CHROMA_API_KEY", "test-key"),
("CHROMA_HOST", "api.devchroma.com"),
],
|| {
let options = ChromaHttpClientOptions::from_cloud_env().unwrap();
assert_eq!(options.endpoint.as_str(), "https://api.devchroma.com/");
},
);
}
}