- Removed unused modules and updated module paths for clarity. - Added autocomplete functionality for command input in InputState. - Updated AppStateRest to include a method for checking if a turn is in flight. - Refactored subagent engine to use new API client structure. - Changed default provider from "openrouter" to "zen" with updated API keys and models. - Implemented tests for memory management and edit log functionalities. - Enhanced error handling in API requests and improved response parsing. - Updated UI components to reflect new API provider and status indicators.
95 lines
3.0 KiB
Rust
95 lines
3.0 KiB
Rust
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<String> {
|
|
let response = self.chat_with_tools(messages, None)?;
|
|
Ok(response.content.unwrap_or_default())
|
|
}
|
|
|
|
pub fn chat_with_tools(
|
|
&self,
|
|
messages: &[ChatMessage],
|
|
tools: Option<Vec<ToolDef>>,
|
|
) -> Result<ChatMessage> {
|
|
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)
|
|
}
|
|
}
|