Skip to main content

llm/providers/openrouter/
provider.rs

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