use crate::providers::traits::Provider; use async_trait::async_trait; use reqwest::Client; use serde::{Deserialize, Serialize}; pub struct OpenRouterProvider { api_key: Option, client: Client, } #[derive(Debug, Serialize)] struct ChatRequest { model: String, messages: Vec, temperature: f64, } #[derive(Debug, Serialize)] struct Message { role: String, content: String, } #[derive(Debug, Deserialize)] struct ChatResponse { choices: Vec, } #[derive(Debug, Deserialize)] struct Choice { message: ResponseMessage, } #[derive(Debug, Deserialize)] struct ResponseMessage { content: String, } impl OpenRouterProvider { pub fn new(api_key: Option<&str>) -> Self { Self { api_key: api_key.map(ToString::to_string), client: Client::builder() .timeout(std::time::Duration::from_secs(120)) .connect_timeout(std::time::Duration::from_secs(10)) .build() .unwrap_or_else(|_| Client::new()), } } } #[async_trait] impl Provider for OpenRouterProvider { async fn warmup(&self) -> anyhow::Result<()> { // Hit a lightweight endpoint to establish TLS + HTTP/2 connection pool. // This prevents the first real chat request from timing out on cold start. if let Some(api_key) = self.api_key.as_ref() { self.client .get("https://openrouter.ai/api/v1/auth/key") .header("Authorization", format!("Bearer {api_key}")) .send() .await? .error_for_status()?; } Ok(()) } async fn chat_with_system( &self, system_prompt: Option<&str>, message: &str, model: &str, temperature: f64, ) -> anyhow::Result { let api_key = self.api_key.as_ref() .ok_or_else(|| anyhow::anyhow!("OpenRouter API key not set. Run `zeroclaw onboard` or set OPENROUTER_API_KEY env var."))?; let mut messages = Vec::new(); if let Some(sys) = system_prompt { messages.push(Message { role: "system".to_string(), content: sys.to_string(), }); } messages.push(Message { role: "user".to_string(), content: message.to_string(), }); let request = ChatRequest { model: model.to_string(), messages, temperature, }; let response = self .client .post("https://openrouter.ai/api/v1/chat/completions") .header("Authorization", format!("Bearer {api_key}")) .header( "HTTP-Referer", "https://github.com/theonlyhennygod/zeroclaw", ) .header("X-Title", "ZeroClaw") .json(&request) .send() .await?; if !response.status().is_success() { let error = response.text().await?; anyhow::bail!("OpenRouter API error: {error}"); } let chat_response: ChatResponse = response.json().await?; chat_response .choices .into_iter() .next() .map(|c| c.message.content) .ok_or_else(|| anyhow::anyhow!("No response from OpenRouter")) } }