diff --git a/Cargo.toml b/Cargo.toml index 7cf4ee9..e960052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quicommit" -version = "0.2.0" +version = "0.2.1" edition = "2024" authors = ["Sidney Zhang "] description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)" diff --git a/src/commands/config.rs b/src/commands/config.rs index 3bd1d86..9ece0ce 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -993,7 +993,7 @@ impl ConfigCommand { "deepseek" => { println!("DeepSeek models:"); println!(" deepseek-chat"); - println!(" deepseek-coder"); + println!(" deepseek-reasoner"); } "openrouter" => { println!("OpenRouter models (examples):"); diff --git a/src/config/mod.rs b/src/config/mod.rs index 6b1f198..b39a401 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -110,6 +110,10 @@ pub struct LlmConfig { /// API key (stored in config for fallback, encrypted if encrypt_sensitive is true) #[serde(default)] pub api_key: Option, + + /// Enable thinking/reasoning mode (deepseek, kimi) + #[serde(default)] + pub thinking_enabled: bool, } fn default_api_key_storage() -> String { @@ -127,6 +131,7 @@ impl Default for LlmConfig { timeout: default_timeout(), api_key_storage: default_api_key_storage(), api_key: None, + thinking_enabled: false, } } } diff --git a/src/llm/deepseek.rs b/src/llm/deepseek.rs index 067ea86..4214187 100644 --- a/src/llm/deepseek.rs +++ b/src/llm/deepseek.rs @@ -10,6 +10,7 @@ pub struct DeepSeekClient { api_key: String, model: String, client: reqwest::Client, + thinking_enabled: bool, } #[derive(Debug, Serialize)] @@ -21,6 +22,14 @@ struct ChatCompletionRequest { #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + thinking: Option, +} + +#[derive(Debug, Serialize)] +struct ThinkingConfig { + #[serde(rename = "type")] + thinking_type: String, } #[derive(Debug, Serialize, Deserialize)] @@ -37,6 +46,8 @@ struct ChatCompletionResponse { #[derive(Debug, Deserialize)] struct Choice { message: Message, + #[serde(default)] + reasoning_content: Option, } #[derive(Debug, Deserialize)] @@ -55,24 +66,26 @@ impl DeepSeekClient { /// Create new DeepSeek client pub fn new(api_key: &str, model: &str) -> Result { let client = create_http_client(Duration::from_secs(60))?; - + Ok(Self { - base_url: "https://api.deepseek.com/v1".to_string(), + base_url: "https://api.deepseek.com/".to_string(), api_key: api_key.to_string(), model: model.to_string(), client, + thinking_enabled: false, }) } /// Create with custom base URL pub fn with_base_url(api_key: &str, model: &str, base_url: &str) -> Result { let client = create_http_client(Duration::from_secs(60))?; - + Ok(Self { base_url: base_url.trim_end_matches('/').to_string(), api_key: api_key.to_string(), model: model.to_string(), client, + thinking_enabled: false, }) } @@ -82,6 +95,12 @@ impl DeepSeekClient { Ok(self) } + /// Enable or disable thinking mode + pub fn with_thinking(mut self, enabled: bool) -> Self { + self.thinking_enabled = enabled; + self + } + /// List available models pub async fn list_models(&self) -> Result> { let url = format!("{}/models", self.base_url); @@ -142,25 +161,25 @@ impl LlmProvider for DeepSeekClient { content: prompt.to_string(), }, ]; - + self.chat_completion(messages).await } async fn generate_with_system(&self, system: &str, user: &str) -> Result { let mut messages = vec![]; - + if !system.is_empty() { messages.push(Message { role: "system".to_string(), content: system.to_string(), }); } - + messages.push(Message { role: "user".to_string(), content: user.to_string(), }); - + self.chat_completion(messages).await } @@ -176,15 +195,24 @@ impl LlmProvider for DeepSeekClient { impl DeepSeekClient { async fn chat_completion(&self, messages: Vec) -> Result { let url = format!("{}/chat/completions", self.base_url); - + + let thinking = if self.thinking_enabled { + Some(ThinkingConfig { + thinking_type: "enabled".to_string(), + }) + } else { + None + }; + let request = ChatCompletionRequest { model: self.model.clone(), messages, max_tokens: Some(500), temperature: Some(0.7), stream: false, + thinking, }; - + let response = self.client .post(&url) .header("Authorization", format!("Bearer {}", self.api_key)) @@ -193,25 +221,24 @@ impl DeepSeekClient { .send() .await .context("Failed to send request to DeepSeek")?; - + let status = response.status(); - + if !status.is_success() { let text = response.text().await.unwrap_or_default(); - - // Try to parse error + if let Ok(error) = serde_json::from_str::(&text) { bail!("DeepSeek API error: {} ({})", error.error.message, error.error.error_type); } - + bail!("DeepSeek API error: {} - {}", status, text); } - + let result: ChatCompletionResponse = response .json() .await .context("Failed to parse DeepSeek response")?; - + result.choices .into_iter() .next() @@ -223,7 +250,7 @@ impl DeepSeekClient { /// Available DeepSeek models pub const DEEPSEEK_MODELS: &[&str] = &[ "deepseek-chat", - "deepseek-coder", + "deepseek-reasoner", ]; /// Check if a model name is valid @@ -238,6 +265,7 @@ mod tests { #[test] fn test_model_validation() { assert!(is_valid_model("deepseek-chat")); + assert!(is_valid_model("deepseek-reasoner")); assert!(!is_valid_model("invalid-model")); } -} \ No newline at end of file +} diff --git a/src/llm/kimi.rs b/src/llm/kimi.rs index 3681dec..08610a6 100644 --- a/src/llm/kimi.rs +++ b/src/llm/kimi.rs @@ -10,6 +10,7 @@ pub struct KimiClient { api_key: String, model: String, client: reqwest::Client, + thinking_enabled: bool, } #[derive(Debug, Serialize)] @@ -21,6 +22,14 @@ struct ChatCompletionRequest { #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, stream: bool, + #[serde(skip_serializing_if = "Option::is_none")] + thinking: Option, +} + +#[derive(Debug, Serialize)] +struct ThinkingConfig { + #[serde(rename = "type")] + thinking_type: String, } #[derive(Debug, Serialize, Deserialize)] @@ -37,6 +46,8 @@ struct ChatCompletionResponse { #[derive(Debug, Deserialize)] struct Choice { message: Message, + #[serde(default)] + reasoning_content: Option, } #[derive(Debug, Deserialize)] @@ -55,24 +66,26 @@ impl KimiClient { /// Create new Kimi client pub fn new(api_key: &str, model: &str) -> Result { let client = create_http_client(Duration::from_secs(60))?; - + Ok(Self { base_url: "https://api.moonshot.cn/v1".to_string(), api_key: api_key.to_string(), model: model.to_string(), client, + thinking_enabled: false, }) } /// Create with custom base URL pub fn with_base_url(api_key: &str, model: &str, base_url: &str) -> Result { let client = create_http_client(Duration::from_secs(60))?; - + Ok(Self { base_url: base_url.trim_end_matches('/').to_string(), api_key: api_key.to_string(), model: model.to_string(), client, + thinking_enabled: false, }) } @@ -82,6 +95,12 @@ impl KimiClient { Ok(self) } + /// Enable or disable thinking mode + pub fn with_thinking(mut self, enabled: bool) -> Self { + self.thinking_enabled = enabled; + self + } + /// List available models pub async fn list_models(&self) -> Result> { let url = format!("{}/models", self.base_url); @@ -142,25 +161,25 @@ impl LlmProvider for KimiClient { content: prompt.to_string(), }, ]; - + self.chat_completion(messages).await } async fn generate_with_system(&self, system: &str, user: &str) -> Result { let mut messages = vec![]; - + if !system.is_empty() { messages.push(Message { role: "system".to_string(), content: system.to_string(), }); } - + messages.push(Message { role: "user".to_string(), content: user.to_string(), }); - + self.chat_completion(messages).await } @@ -176,15 +195,24 @@ impl LlmProvider for KimiClient { impl KimiClient { async fn chat_completion(&self, messages: Vec) -> Result { let url = format!("{}/chat/completions", self.base_url); - + + let thinking = if self.thinking_enabled { + Some(ThinkingConfig { + thinking_type: "enabled".to_string(), + }) + } else { + None + }; + let request = ChatCompletionRequest { model: self.model.clone(), messages, max_tokens: Some(500), - temperature: Some(0.7), + temperature: Some(1.0), stream: false, + thinking, }; - + let response = self.client .post(&url) .header("Authorization", format!("Bearer {}", self.api_key)) @@ -193,25 +221,24 @@ impl KimiClient { .send() .await .context("Failed to send request to Kimi")?; - + let status = response.status(); - + if !status.is_success() { let text = response.text().await.unwrap_or_default(); - - // Try to parse error + if let Ok(error) = serde_json::from_str::(&text) { bail!("Kimi API error: {} ({})", error.error.message, error.error.error_type); } - + bail!("Kimi API error: {} - {}", status, text); } - + let result: ChatCompletionResponse = response .json() .await .context("Failed to parse Kimi response")?; - + result.choices .into_iter() .next() @@ -241,4 +268,4 @@ mod tests { assert!(is_valid_model("moonshot-v1-8k")); assert!(!is_valid_model("invalid-model")); } -} \ No newline at end of file +} diff --git a/src/llm/mod.rs b/src/llm/mod.rs index d3094b8..2b05430 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -44,6 +44,7 @@ pub struct LlmClientConfig { pub max_tokens: u32, pub temperature: f32, pub timeout: Duration, + pub thinking_enabled: bool, } impl Default for LlmClientConfig { @@ -52,6 +53,7 @@ impl Default for LlmClientConfig { max_tokens: 500, temperature: 0.7, timeout: Duration::from_secs(30), + thinking_enabled: false, } } } @@ -64,12 +66,14 @@ impl LlmClient { max_tokens: config.llm.max_tokens, temperature: config.llm.temperature, timeout: Duration::from_secs(config.llm.timeout), + thinking_enabled: config.llm.thinking_enabled, }; let provider = config.llm.provider.as_str(); let model = config.llm.model.as_str(); let base_url = manager.llm_base_url(); let api_key = manager.get_api_key(); + let thinking_enabled = config.llm.thinking_enabled; let provider: Box = match provider { "ollama" => { @@ -88,12 +92,12 @@ impl LlmClient { "kimi" => { let key = api_key.as_ref() .ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?; - Box::new(KimiClient::with_base_url(key, model, &base_url)?) + Box::new(KimiClient::with_base_url(key, model, &base_url)?.with_thinking(thinking_enabled)) } "deepseek" => { let key = api_key.as_ref() .ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?; - Box::new(DeepSeekClient::with_base_url(key, model, &base_url)?) + Box::new(DeepSeekClient::with_base_url(key, model, &base_url)?.with_thinking(thinking_enabled)) } "openrouter" => { let key = api_key.as_ref()