use crate::error::{Error, Result};
use crate::types::routing::{PredefinedModelCoverageProfile, RouterConfig};
use std::marker::PhantomData;
use std::time::Duration;
use url::Url;
pub mod config;
pub use config::*;
pub const ROUTING_NITRO: &str = ":nitro";
pub const ROUTING_FLOOR: &str = ":floor";
pub const ROUTING_ONLINE: &str = ":online";
#[derive(Debug)]
pub struct Unconfigured;
#[derive(Debug)]
pub struct NoAuth;
#[derive(Debug)]
pub struct Ready;
#[derive(Debug)]
pub struct OpenRouterClient<State = Unconfigured> {
pub(crate) config: ClientConfig,
pub(crate) http_client: Option<reqwest::Client>,
pub(crate) _state: PhantomData<State>,
pub(crate) router_config: Option<RouterConfig>,
pub(crate) cached_api_config: Option<ApiConfig>,
pub(crate) providers_cache: Option<
std::sync::Arc<
std::sync::Mutex<
crate::utils::cache::Cache<String, crate::types::providers::ProvidersResponse>,
>,
>,
>,
}
impl Default for OpenRouterClient<Unconfigured> {
fn default() -> Self {
Self::new()
}
}
impl OpenRouterClient<Ready> {
#[must_use = "returns a configured client that should be used for API calls"]
pub fn from_env() -> Result<Self> {
let api_key = crate::utils::auth::load_api_key_from_env()?;
OpenRouterClient::from_api_key(api_key)
}
#[must_use = "returns a configured client that should be used for API calls"]
pub fn from_env_with_config(
referer: Option<impl Into<String>>,
title: Option<impl Into<String>>,
) -> Result<Self> {
let api_key = crate::utils::auth::load_api_key_from_env()?;
OpenRouterClient::new()
.skip_url_configuration()
.configure(api_key, referer, title)
}
#[must_use = "returns a configured client that should be used for API calls"]
pub fn quick() -> Result<Self> {
Self::from_env()
}
#[must_use = "returns a configured client that should be used for API calls"]
pub fn production(
api_key: impl Into<String>,
app_name: impl Into<String>,
app_url: impl Into<String>,
) -> Result<Self> {
OpenRouterClient::new()
.skip_url_configuration()
.with_timeout_secs(60) .with_retries(5, 1000) .configure(api_key, Some(app_url), Some(app_name))
}
}
impl OpenRouterClient<Unconfigured> {
#[must_use = "returns a client builder that should be configured and used for API calls"]
pub fn new() -> Self {
Self {
config: ClientConfig {
api_key: None,
base_url: "https://openrouter.ai/api/v1/".parse().unwrap(),
http_referer: None,
site_title: None,
user_id: None,
timeout: Duration::from_secs(30),
retry_config: RetryConfig::default(),
max_response_bytes: 10 * 1024 * 1024,
},
http_client: None,
_state: PhantomData,
router_config: None,
cached_api_config: None,
providers_cache: None,
}
}
#[must_use = "returns a configured client that should be used for API calls"]
pub fn from_api_key(api_key: impl Into<String>) -> Result<OpenRouterClient<Ready>> {
Self::new().skip_url_configuration().with_api_key(api_key)
}
#[must_use = "returns a configured client that should be used for API calls"]
pub fn from_api_key_and_url(
api_key: impl Into<String>,
base_url: impl Into<String>,
) -> Result<OpenRouterClient<Ready>> {
Self::new().with_base_url(base_url)?.with_api_key(api_key)
}
#[must_use = "returns the updated client that should be configured and used for API calls"]
pub fn skip_url_configuration(self) -> OpenRouterClient<NoAuth> {
self.transition_to_no_auth()
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_base_url(
mut self,
base_url: impl Into<String>,
) -> Result<OpenRouterClient<NoAuth>> {
let url_str = base_url.into();
self.config.base_url = Url::parse(&url_str).map_err(|e| {
Error::ConfigError(format!(
"Invalid base URL '{url_str}': {e}. Expected format: 'https://api.example.com/v1/'"
))
})?;
crate::utils::https::enforce_https(&self.config.base_url)?;
Ok(self.transition_to_no_auth())
}
fn transition_to_no_auth(self) -> OpenRouterClient<NoAuth> {
OpenRouterClient {
config: self.config,
http_client: None,
_state: PhantomData,
router_config: self.router_config,
cached_api_config: None,
providers_cache: None,
}
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_max_response_bytes(mut self, bytes: usize) -> Self {
self.config.max_response_bytes = bytes;
self
}
#[must_use = "returns a formatted model ID string that should be used in requests"]
pub fn model_with_shortcut(model: &str, shortcut: &str) -> String {
format!("{}{}", model, shortcut)
}
}
impl OpenRouterClient<NoAuth> {
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Result<OpenRouterClient<Ready>> {
self.config.api_key = Some(SecureApiKey::new(api_key)?);
self.transition_to_ready()
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_secure_api_key(mut self, api_key: SecureApiKey) -> Result<OpenRouterClient<Ready>> {
self.config.api_key = Some(api_key);
self.transition_to_ready()
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn configure(
mut self,
api_key: impl Into<String>,
referer: Option<impl Into<String>>,
title: Option<impl Into<String>>,
) -> Result<OpenRouterClient<Ready>> {
if let Some(ref_val) = referer {
self.config.http_referer = Some(ref_val.into());
}
if let Some(title_val) = title {
self.config.site_title = Some(title_val.into());
}
self.config.api_key = Some(SecureApiKey::new(api_key)?);
self.transition_to_ready()
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_timeout_secs(mut self, seconds: u64) -> Self {
self.config.timeout = Duration::from_secs(seconds);
self
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_http_referer(mut self, referer: impl Into<String>) -> Self {
self.config.http_referer = Some(referer.into());
self
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_site_title(mut self, title: impl Into<String>) -> Self {
self.config.site_title = Some(title.into());
self
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.config.user_id = Some(user_id.into());
self
}
#[must_use = "returns the updated client that should be used for API calls"]
pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
self.config.retry_config = retry_config;
self
}
#[must_use = "returns updated client that should be used for API calls"]
pub fn with_retries(mut self, max_retries: u32, initial_backoff_ms: u64) -> Self {
self.config.retry_config.max_retries = max_retries;
self.config.retry_config.initial_backoff_ms = initial_backoff_ms;
self
}
#[must_use = "returns updated client that should be used for API calls"]
pub fn with_max_response_bytes(mut self, bytes: usize) -> Self {
self.config.max_response_bytes = bytes;
self
}
#[must_use = "returns updated client that should be used for API calls"]
pub fn without_retries(mut self) -> Self {
self.config.retry_config.max_retries = 0;
self
}
#[must_use = "returns updated client that should be used for API calls"]
pub fn with_model_coverage_profile(mut self, profile: PredefinedModelCoverageProfile) -> Self {
self.router_config = Some(RouterConfig {
profile,
provider_preferences: None,
});
self
}
#[must_use = "returns updated client that should be used for API calls"]
pub fn with_zdr(mut self) -> Self {
let router_config = self.router_config.get_or_insert(RouterConfig {
profile: PredefinedModelCoverageProfile::LowestCost,
provider_preferences: Some(crate::types::provider::ProviderPreferences::new()),
});
let prefs = router_config
.provider_preferences
.get_or_insert(crate::types::provider::ProviderPreferences::new());
prefs.data_collection = Some("deny".to_string());
self
}
fn transition_to_ready(self) -> Result<OpenRouterClient<Ready>> {
let headers = self.config.build_headers()?;
let client_builder = reqwest::Client::builder()
.timeout(self.config.timeout)
.tcp_keepalive(Duration::from_secs(60))
.default_headers(headers);
let http_client = client_builder
.build()
.map_err(|e| Error::ConfigError(format!("Failed to create HTTP client: {e}")))?;
let api_config = self.config.to_api_config()?;
Ok(OpenRouterClient {
config: self.config,
http_client: Some(http_client),
_state: PhantomData,
router_config: self.router_config,
cached_api_config: Some(api_config),
providers_cache: Some(std::sync::Arc::new(std::sync::Mutex::new(
crate::utils::cache::Cache::new(std::time::Duration::from_secs(300)),
))),
})
}
}
impl OpenRouterClient<Ready> {
fn get_client_and_config(&self) -> Result<(reqwest::Client, ApiConfig)> {
let client = self
.http_client
.clone()
.ok_or_else(|| Error::ConfigError("HTTP client is missing".into()))?;
let api_config = self
.cached_api_config
.clone()
.ok_or_else(|| Error::ConfigError("API config is missing".into()))?;
Ok((client, api_config))
}
pub fn chat(&self) -> Result<crate::api::chat::ChatApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::chat::ChatApi { client, config })
}
pub fn completions(&self) -> Result<crate::api::completion::CompletionApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::completion::CompletionApi { client, config })
}
pub fn models(&self) -> Result<crate::api::models::ModelsApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::models::ModelsApi { client, config })
}
pub fn structured(&self) -> Result<crate::api::structured::StructuredApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::structured::StructuredApi { client, config })
}
pub fn web_search(&self) -> Result<crate::api::web_search::WebSearchApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::web_search::WebSearchApi { client, config })
}
pub fn credits(&self) -> Result<crate::api::credits::CreditsApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::credits::CreditsApi { client, config })
}
pub fn analytics(&self) -> Result<crate::api::analytics::AnalyticsApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::analytics::AnalyticsApi { client, config })
}
pub fn providers(&self) -> Result<crate::api::providers::ProvidersApi> {
let (client, config) = self.get_client_and_config()?;
let cache = self.providers_cache.clone().ok_or_else(|| {
crate::error::Error::ConfigError("Providers cache not initialized".into())
})?;
Ok(crate::api::providers::ProvidersApi {
client,
config,
cache,
})
}
pub fn key_info(&self) -> Result<crate::api::key_info::KeyInfoApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::key_info::KeyInfoApi { client, config })
}
pub fn embeddings(&self) -> Result<crate::api::embeddings::EmbeddingsApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::embeddings::EmbeddingsApi { client, config })
}
pub fn generation(&self) -> Result<crate::api::generation::GenerationApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::generation::GenerationApi { client, config })
}
pub fn guardrails(&self) -> Result<crate::api::guardrails::GuardrailsApi> {
let (client, config) = self.get_client_and_config()?;
Ok(crate::api::guardrails::GuardrailsApi { client, config })
}
pub fn chat_request_builder(
&self,
messages: Vec<crate::types::chat::Message>,
) -> crate::api::request::RequestBuilder<serde_json::Value> {
let primary_model = if let Some(router_config) = &self.router_config {
match &router_config.profile {
PredefinedModelCoverageProfile::Custom(profile) => profile.primary.clone(),
PredefinedModelCoverageProfile::LowestLatency => "openai/gpt-3.5-turbo".to_string(),
PredefinedModelCoverageProfile::LowestCost => "openai/gpt-3.5-turbo".to_string(),
PredefinedModelCoverageProfile::HighestQuality => {
"anthropic/claude-3-opus-20240229".to_string()
}
}
} else {
"openai/gpt-4o".to_string()
};
let mut extra_params = serde_json::json!({});
if let Some(router_config) = &self.router_config {
if let Some(provider_prefs) = &router_config.provider_preferences {
if let Ok(prefs_value) = serde_json::to_value(provider_prefs) {
extra_params["provider"] = prefs_value;
}
}
if let PredefinedModelCoverageProfile::Custom(profile) = &router_config.profile {
if let Some(fallbacks) = &profile.fallbacks {
if let Ok(fallbacks_value) = serde_json::to_value(fallbacks) {
extra_params["models"] = fallbacks_value;
}
}
}
}
crate::api::request::RequestBuilder::new(primary_model, messages, extra_params)
}
pub fn validate_tool_calls(
&self,
_response: &crate::types::chat::ChatCompletionResponse,
) -> Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests;