use super::openai_compat::{OpenAiCompatAdapter, OpenAiCompatConfig};
use crate::inference::adapter::InferenceAdapter;
use crate::inference::configurator::ResolvedProvider;
use crate::inference::error::InferenceError;
use crate::inference::registry::ProviderId;
pub const OPENROUTER_BASE_URL: &str = "https://openrouter.ai/api/v1";
const OPENROUTER_REFERER: &str = "https://github.com/bobmatnyc/trusty-tools";
const OPENROUTER_TITLE: &str = "trusty-tools";
pub fn build(
resolved: &ResolvedProvider,
base_url: &str,
) -> Result<Box<dyn InferenceAdapter>, InferenceError> {
let key = resolved
.key()
.ok_or(InferenceError::MissingCredential {
provider: ProviderId::OpenRouter,
})?
.clone();
let config = OpenAiCompatConfig {
name: ProviderId::OpenRouter.as_str().to_string(),
base_url: base_url.to_string(),
api_key: key,
extra_headers: vec![
("HTTP-Referer".to_string(), OPENROUTER_REFERER.to_string()),
("X-Title".to_string(), OPENROUTER_TITLE.to_string()),
],
capabilities: *resolved.capabilities(),
};
Ok(Box::new(OpenAiCompatAdapter::new(config)?))
}
pub fn factory(resolved: &ResolvedProvider) -> Result<Box<dyn InferenceAdapter>, InferenceError> {
build(resolved, OPENROUTER_BASE_URL)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inference::registry::capabilities;
use crate::inference::types::{ChatMessage, ChatRequest, SecretString};
fn resolved(key: &str) -> ResolvedProvider {
ResolvedProvider::new(
ProviderId::OpenRouter,
"openai/gpt-4o-mini".to_string(),
Some(SecretString::new(key)),
)
}
#[test]
fn factory_builds_named_adapter() {
let adapter = build(&resolved("sk-or-test"), OPENROUTER_BASE_URL) .expect("built");
assert_eq!(adapter.name(), "openrouter");
assert!(adapter.wants_detailed_usage());
assert_eq!(adapter.capabilities().id, ProviderId::OpenRouter);
}
#[test]
fn missing_key_errors() {
let resolved = ResolvedProvider::new(
ProviderId::OpenRouter,
"openai/gpt-4o-mini".to_string(),
None,
);
let Err(err) = build(&resolved, OPENROUTER_BASE_URL) else {
panic!("expected MissingCredential");
};
assert!(matches!(
err,
InferenceError::MissingCredential {
provider: ProviderId::OpenRouter
}
));
}
#[tokio::test]
#[ignore = "requires OPENROUTER_API_KEY; skipped in CI"]
async fn live_openrouter_call() {
let Ok(key) = std::env::var("OPENROUTER_API_KEY") else {
eprintln!("OPENROUTER_API_KEY not set — skipping live test");
return;
};
if key.trim().is_empty() {
eprintln!("OPENROUTER_API_KEY is empty — skipping live test");
return;
}
let resolved = ResolvedProvider::new(
ProviderId::OpenRouter,
"openai/gpt-4o-mini".to_string(),
Some(SecretString::new(key)),
);
let adapter = build(&resolved, OPENROUTER_BASE_URL).expect("build adapter");
let _ = capabilities(ProviderId::OpenRouter);
let mut req = ChatRequest::new(
"openai/gpt-4o-mini",
vec![
ChatMessage::system("You are a concise assistant."),
ChatMessage::user("Reply with exactly the word: pong"),
],
);
req.temperature = Some(0.0);
req.max_tokens = Some(16);
let resp = adapter.chat(&req).await.expect("live chat");
let text = resp.first_text().expect("assistant text");
assert!(!text.is_empty(), "assistant text was empty");
assert!(
resp.usage().prompt_tokens > 0,
"prompt_tokens should be > 0"
);
eprintln!(
"live openrouter ok — text: {text:?}, usage: {:?}",
resp.usage()
);
}
}