use std::time::Duration; use tracing::info; use reqwest::Client; use serde::{Deserialize, Serialize}; use crate::error::Result; use crate::lang; pub struct OpenRouterClient { api_key: String, model: String, api_url: String, client: Client, } #[derive(Serialize)] struct ChatRequest { model: String, messages: Vec, max_tokens: u32, } #[derive(Serialize)] struct Message { role: String, content: String, } #[derive(Deserialize)] struct ChatResponse { choices: Vec, } #[derive(Deserialize)] struct Choice { message: MessageContent, } #[derive(Deserialize)] struct MessageContent { content: String, } impl OpenRouterClient { /// `api_url` is the full URL including `/chat/completions` path. pub fn new(api_url: String, api_key: String, model: String) -> Self { info!("LLM client: url={api_url}, model={model}"); Self { api_key, model, api_url, client: Client::builder() .connect_timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(60)) .build() .unwrap_or_default(), } } async fn do_request(&self, body: &ChatRequest) -> Result { let mut req = self .client .post(&self.api_url) .header("Content-Type", "application/json") .json(body); if !self.api_key.is_empty() { req = req.header("Authorization", format!("Bearer {}", self.api_key)); } let resp = req.send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(crate::error::Error::Custom(lang::fmt_llm_err( &self.api_url, &status, &text, ))); } let chat_resp: ChatResponse = resp.json().await?; Ok(chat_resp .choices .into_iter() .next() .map(|c| c.message.content) .unwrap_or_default()) } pub async fn chat(&self, system_prompt: &str, user_message: &str) -> Result { let body = ChatRequest { model: self.model.clone(), messages: vec![ Message { role: "system".into(), content: system_prompt.to_string(), }, Message { role: "user".into(), content: user_message.to_string(), }, ], max_tokens: 500, }; self.do_request(&body).await } pub async fn chat_with_history( &self, system_prompt: &str, messages: Vec<(String, String)>, // (role, content) pairs ) -> Result { let mut msg_vec = vec![Message { role: "system".into(), content: system_prompt.to_string(), }]; for (role, content) in messages { msg_vec.push(Message { role, content }); } let body = ChatRequest { model: self.model.clone(), messages: msg_vec, max_tokens: 500, }; self.do_request(&body).await } }