use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::Serialize;
use crate::config::ClientConfig;
use crate::error::OpenAIError;
pub fn config(api_key: &str, app: Option<App<'_>>) -> Result<ClientConfig, OpenAIError> {
let mut cfg = ClientConfig::new(api_key).base_url("https://openrouter.ai/api/v1");
if let Some(app) = app {
let mut headers = HeaderMap::with_capacity(2);
if !app.url.is_empty() {
set(&mut headers, "http-referer", app.url)?;
}
if !app.name.is_empty() {
set(&mut headers, "x-openrouter-title", app.name)?;
}
if !headers.is_empty() {
cfg = cfg.default_headers(headers);
}
}
Ok(cfg)
}
pub struct App<'a> {
pub name: &'a str,
pub url: &'a str,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct ProviderPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub only: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_fallbacks: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_parameters: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantizations: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub zdr: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_collection: Option<String>,
}
impl ProviderPreferences {
pub fn pinned(provider: &str) -> Self {
Self {
only: Some(vec![provider.to_string()]),
allow_fallbacks: Some(false),
require_parameters: Some(true),
..Default::default()
}
}
pub fn prefer(providers: &[&str]) -> Self {
Self {
order: Some(providers.iter().map(|s| s.to_string()).collect()),
require_parameters: Some(true),
..Default::default()
}
}
pub fn fast() -> Self {
Self {
sort: Some("throughput".to_string()),
require_parameters: Some(true),
..Default::default()
}
}
pub fn cheap() -> Self {
Self {
sort: Some("price".to_string()),
require_parameters: Some(true),
..Default::default()
}
}
pub fn to_value(&self) -> Result<serde_json::Value, OpenAIError> {
serde_json::to_value(self).map_err(|e| {
OpenAIError::InvalidArgument(format!("failed to serialize provider preferences: {e}"))
})
}
}
pub fn inject_provider(
body: &mut serde_json::Value,
prefs: &ProviderPreferences,
) -> Result<(), OpenAIError> {
let provider_value = prefs.to_value()?;
if let serde_json::Value::Object(map) = body {
map.insert("provider".to_string(), provider_value);
}
Ok(())
}
fn set(headers: &mut HeaderMap, name: &'static str, value: &str) -> Result<(), OpenAIError> {
headers.insert(
HeaderName::from_static(name),
HeaderValue::from_str(value)
.map_err(|e| OpenAIError::InvalidArgument(format!("invalid header {name}: {e}")))?,
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_url() {
let cfg = config("sk-or-test", None).unwrap();
assert_eq!(cfg.base_url, "https://openrouter.ai/api/v1");
}
#[test]
fn test_bearer_auth() {
let cfg = config("sk-or-test", None).unwrap();
assert_eq!(cfg.api_key, "sk-or-test");
}
#[test]
fn test_app_headers() {
let cfg = config(
"sk-or-test",
Some(App {
name: "my-agent",
url: "https://example.com",
}),
)
.unwrap();
let headers = cfg.default_headers.as_ref().unwrap();
assert_eq!(headers.get("http-referer").unwrap(), "https://example.com");
assert_eq!(headers.get("x-openrouter-title").unwrap(), "my-agent");
}
#[test]
fn test_no_app_no_headers() {
let cfg = config("sk-or-test", None).unwrap();
assert!(cfg.default_headers.is_none());
}
#[test]
fn test_pinned_provider() {
let prefs = ProviderPreferences::pinned("anthropic");
let v = prefs.to_value().unwrap();
assert_eq!(v["only"], serde_json::json!(["anthropic"]));
assert_eq!(v["allow_fallbacks"], serde_json::json!(false));
assert_eq!(v["require_parameters"], serde_json::json!(true));
assert!(v.get("order").is_none());
}
#[test]
fn test_prefer_providers() {
let prefs = ProviderPreferences::prefer(&["together", "openai"]);
let v = prefs.to_value().unwrap();
assert_eq!(v["order"], serde_json::json!(["together", "openai"]));
assert!(v.get("only").is_none());
}
#[test]
fn test_inject_provider() {
let mut body = serde_json::json!({
"model": "anthropic/claude-sonnet-4-6",
"messages": [{"role": "user", "content": "hi"}]
});
inject_provider(&mut body, &ProviderPreferences::pinned("anthropic")).unwrap();
assert_eq!(body["provider"]["only"], serde_json::json!(["anthropic"]));
assert_eq!(
body["provider"]["allow_fallbacks"],
serde_json::json!(false)
);
assert_eq!(body["model"], "anthropic/claude-sonnet-4-6");
}
#[test]
fn test_fast_sort() {
let prefs = ProviderPreferences::fast();
let v = prefs.to_value().unwrap();
assert_eq!(v["sort"], "throughput");
}
}