use std::time::Duration; use anyhow::Result; use crate::dto::chat::message::ChatMessage; use crate::dto::provider::request::ToolDef; const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1"; const DEFAULT_MODEL: &str = "deepseek-v4-flash-free"; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); pub struct LlmClient { pub client: reqwest::blocking::Client, pub api_key: String, pub base_url: String, pub model: String, } impl LlmClient { pub fn new(api_key: String, model: String) -> Self { let model = if model.is_empty() { DEFAULT_MODEL.to_string() } else { model }; let client = reqwest::blocking::Client::builder() .timeout(REQUEST_TIMEOUT) .connect_timeout(CONNECT_TIMEOUT) .build() .unwrap_or_else(|_| reqwest::blocking::Client::new()); LlmClient { client, api_key, base_url: DEFAULT_BASE_URL.to_string(), model, } } pub fn chat(&self, messages: &[ChatMessage]) -> Result { let response = self.chat_with_tools(messages, None)?; Ok(response.content.unwrap_or_default()) } pub fn chat_with_tools( &self, messages: &[ChatMessage], tools: Option>, ) -> Result { let req = crate::dto::provider::request::ChatRequest { model: self.model.clone(), messages: messages.to_vec(), max_tokens: Some(4096), temperature: Some(0.7), tools, stream: Some(false), top_p: None, stop: None, }; let url = format!("{}/chat/completions", self.base_url); let mut http_req = self.client .post(&url) .header("Content-Type", "application/json"); if !self.api_key.is_empty() { http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key)); } let resp = http_req.json(&req).send().map_err(|e| { if e.is_timeout() { anyhow::anyhow!("API request timed out after {:?}. Check your network or try again.", REQUEST_TIMEOUT) } else if e.is_connect() { anyhow::anyhow!("Could not connect to {}. Is the URL correct and is the service reachable?", self.base_url) } else { anyhow::anyhow!("API request failed: {}", e) } })?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().unwrap_or_default(); anyhow::bail!("API error {} from {}: {}", status, self.base_url, body); } let data: crate::dto::provider::response::ChatResponse = resp.json()?; let message = data .choices .into_iter() .next() .map(|c| c.message) .ok_or_else(|| anyhow::anyhow!("API response had no choices"))?; Ok(message) } }