Skip to main content

llm/providers/openrouter/
provider.rs

1use super::types::OpenRouterChatRequest;
2use crate::provider::get_context_window;
3use crate::providers::openai_compatible::{
4    AetherOpenAiConfig, build_chat_request, streaming::create_custom_stream_generic,
5};
6use crate::{
7    Context, LlmError, LlmResponseStream, ProviderAuthMode, ProviderConnectionConfig, ProviderFactory, Result,
8    StreamingModelProvider,
9};
10use async_openai::{Client, config::OpenAIConfig};
11
12pub struct OpenRouterProvider {
13    client: Client<AetherOpenAiConfig>,
14    model: String,
15}
16
17impl OpenRouterProvider {
18    pub fn new(api_key: String, model: String) -> Result<Self> {
19        let config = openai_config(Some(api_key), ProviderConnectionConfig::default());
20
21        let client = Client::with_config(config);
22        Ok(Self { client, model })
23    }
24
25    pub fn default(model: &str) -> Result<Self> {
26        let api_key = std::env::var("OPENROUTER_API_KEY")
27            .map_err(|_| LlmError::MissingApiKey("OPENROUTER_API_KEY".to_string()))?;
28
29        let config = openai_config(Some(api_key), ProviderConnectionConfig::default());
30
31        let client = Client::with_config(config);
32
33        Ok(Self { client, model: model.to_string() })
34    }
35}
36
37fn openai_config(api_key: Option<String>, connection: ProviderConnectionConfig) -> AetherOpenAiConfig {
38    let api_key = api_key.unwrap_or_default();
39    let api_base = connection.base_url.unwrap_or_else(|| "https://openrouter.ai/api/v1".to_string());
40    let config = OpenAIConfig::new().with_api_key(api_key).with_api_base(api_base);
41    AetherOpenAiConfig::new(config, connection.auth_mode)
42}
43
44impl ProviderFactory for OpenRouterProvider {
45    async fn from_env() -> Result<Self> {
46        Self::from_env_with_connection(ProviderConnectionConfig::default()).await
47    }
48
49    async fn from_env_with_connection(connection: ProviderConnectionConfig) -> Result<Self> {
50        let api_key = match connection.auth_mode {
51            ProviderAuthMode::Default => Some(
52                std::env::var("OPENROUTER_API_KEY")
53                    .map_err(|_| LlmError::MissingApiKey("OPENROUTER_API_KEY".to_string()))?,
54            ),
55            ProviderAuthMode::None => None,
56        };
57        let config = openai_config(api_key, connection);
58
59        let client = Client::with_config(config);
60
61        Ok(Self { client, model: String::new() })
62    }
63
64    fn with_model(mut self, model: &str) -> Self {
65        self.model = model.to_string();
66        self
67    }
68}
69
70impl StreamingModelProvider for OpenRouterProvider {
71    fn model(&self) -> Option<crate::LlmModel> {
72        format!("openrouter:{}", self.model).parse().ok()
73    }
74
75    fn context_window(&self) -> Option<u32> {
76        get_context_window("openrouter", &self.model)
77    }
78
79    fn stream_response(&self, context: &Context) -> LlmResponseStream {
80        // Build base request and convert to OpenRouter-specific format
81        // The From trait automatically adds usage tracking parameters
82        // See: https://openrouter.ai/docs/use-cases/usage-accounting
83        let mut request: OpenRouterChatRequest = match build_chat_request(&self.model, context, None) {
84            Ok(req) => req.into(),
85            Err(e) => return Box::pin(async_stream::stream! { yield Err(e); }),
86        };
87
88        if let Some(effort) = context.reasoning_effort() {
89            request.reasoning_effort = Some(effort);
90        }
91
92        create_custom_stream_generic(&self.client, request)
93    }
94
95    fn display_name(&self) -> String {
96        format!("OpenRouter ({})", self.model)
97    }
98}