llm/providers/openrouter/
provider.rs1use 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 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}