refactor(llm): 删除手写 provider 实现与冗余依赖
- 删除 6 个手写 provider 文件、LlmProvider trait、动态分发、HTTP 客户端工厂与门面死代码 - llm 模块收敛为 rig 门面 + 提示词/解析/思考状态四个模块 - 移除 reqwest(0.12) 与 async-trait 直接依赖(futures-util 由流式消费保留) - 依赖树确认无 rig-agent/fastembed/lancedb/milvus 等组件
This commit is contained in:
341
src/llm/mod.rs
341
src/llm/mod.rs
@@ -1,327 +1,14 @@
|
||||
use crate::config::Language;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod anthropic;
|
||||
pub mod deepseek;
|
||||
pub mod kimi;
|
||||
pub mod ollama;
|
||||
pub mod openai;
|
||||
pub mod openrouter;
|
||||
pub mod parsing;
|
||||
pub mod prompts;
|
||||
pub mod rig;
|
||||
pub mod thinking;
|
||||
|
||||
pub use anthropic::AnthropicClient;
|
||||
pub use deepseek::DeepSeekClient;
|
||||
pub use kimi::KimiClient;
|
||||
pub use ollama::OllamaClient;
|
||||
pub use openai::OpenAiClient;
|
||||
pub use openrouter::OpenRouterClient;
|
||||
pub use parsing::GeneratedCommit;
|
||||
|
||||
/// LLM provider trait
|
||||
#[async_trait]
|
||||
pub trait LlmProvider: Send + Sync {
|
||||
/// Generate text from prompt
|
||||
async fn generate(&self, prompt: &str) -> Result<String>;
|
||||
|
||||
/// Generate with system prompt
|
||||
async fn generate_with_system(&self, system: &str, user: &str) -> Result<String>;
|
||||
|
||||
/// Check if provider is available
|
||||
async fn is_available(&self) -> bool;
|
||||
|
||||
/// Get provider name
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// LLM client that wraps different providers
|
||||
pub struct LlmClient {
|
||||
provider: Box<dyn LlmProvider>,
|
||||
config: LlmClientConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LlmClientConfig {
|
||||
pub max_tokens: u32,
|
||||
pub temperature: f32,
|
||||
pub timeout: Duration,
|
||||
pub thinking_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for LlmClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_tokens: 500,
|
||||
temperature: 0.7,
|
||||
timeout: Duration::from_secs(30),
|
||||
thinking_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LlmClient {
|
||||
/// Create LLM client from configuration manager
|
||||
pub async fn from_config(manager: &crate::config::manager::ConfigManager) -> Result<Self> {
|
||||
Self::from_config_with_think(manager, manager.config().llm.thinking_enabled).await
|
||||
}
|
||||
|
||||
/// Create LLM client from configuration with explicit thinking override
|
||||
pub async fn from_config_with_think(
|
||||
manager: &crate::config::manager::ConfigManager,
|
||||
thinking_enabled: bool,
|
||||
) -> Result<Self> {
|
||||
let config = manager.config();
|
||||
let client_config = LlmClientConfig {
|
||||
max_tokens: config.llm.max_tokens,
|
||||
temperature: config.llm.temperature,
|
||||
timeout: Duration::from_secs(config.llm.timeout),
|
||||
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 provider: Box<dyn LlmProvider> = match provider {
|
||||
"ollama" => Box::new(
|
||||
OllamaClient::new(&base_url, model)
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature),
|
||||
),
|
||||
"openai" => {
|
||||
let key = api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenAI API key not configured"))?;
|
||||
let thinking_state = if thinking_enabled {
|
||||
Some(thinking::create_console_thinking_state())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut client = OpenAiClient::new(&base_url, key, model)?
|
||||
.with_thinking(thinking_enabled)
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature)
|
||||
.with_timeout(client_config.timeout)?;
|
||||
if let Some(state) = thinking_state {
|
||||
client = client.with_thinking_state(state);
|
||||
}
|
||||
Box::new(client)
|
||||
}
|
||||
"anthropic" => {
|
||||
let key = api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Anthropic API key not configured"))?;
|
||||
let thinking_state = if thinking_enabled {
|
||||
Some(thinking::create_console_thinking_state())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let budget = config.llm.thinking_budget_tokens.unwrap_or(1024);
|
||||
let mut client = AnthropicClient::new(key, model)?
|
||||
.with_thinking(thinking_enabled)
|
||||
.with_thinking_budget_tokens(budget)
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature)
|
||||
.with_timeout(client_config.timeout)?;
|
||||
if let Some(state) = thinking_state {
|
||||
client = client.with_thinking_state(state);
|
||||
}
|
||||
Box::new(client)
|
||||
}
|
||||
"kimi" => {
|
||||
let key = api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?;
|
||||
let thinking_state = if thinking_enabled {
|
||||
Some(thinking::create_console_thinking_state())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut client = KimiClient::with_base_url(key, model, &base_url)?
|
||||
.with_thinking(thinking_enabled)
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature)
|
||||
.with_timeout(client_config.timeout)?;
|
||||
if let Some(state) = thinking_state {
|
||||
client = client.with_thinking_state(state);
|
||||
}
|
||||
Box::new(client)
|
||||
}
|
||||
"deepseek" => {
|
||||
let key = api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?;
|
||||
let thinking_state = if thinking_enabled {
|
||||
Some(thinking::create_console_thinking_state())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut client = DeepSeekClient::with_base_url(key, model, &base_url)?
|
||||
.with_thinking(thinking_enabled)
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature)
|
||||
.with_timeout(client_config.timeout)?;
|
||||
if let Some(state) = thinking_state {
|
||||
client = client.with_thinking_state(state);
|
||||
}
|
||||
Box::new(client)
|
||||
}
|
||||
"openrouter" => {
|
||||
let key = api_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("OpenRouter API key not configured"))?;
|
||||
Box::new(
|
||||
OpenRouterClient::with_base_url(key, model, &base_url)?
|
||||
.with_max_tokens(client_config.max_tokens)
|
||||
.with_temperature(client_config.temperature)
|
||||
.with_timeout(client_config.timeout)?,
|
||||
)
|
||||
}
|
||||
_ => bail!("Unknown LLM provider: {}", provider),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
provider,
|
||||
config: client_config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with specific provider
|
||||
pub fn with_provider(provider: Box<dyn LlmProvider>) -> Self {
|
||||
Self {
|
||||
provider,
|
||||
config: LlmClientConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate commit message from git diff
|
||||
pub async fn generate_commit_message(
|
||||
&self,
|
||||
diff: &str,
|
||||
format: crate::config::CommitFormat,
|
||||
language: Language,
|
||||
template: Option<&str>,
|
||||
) -> Result<GeneratedCommit> {
|
||||
let mut system_prompt = prompts::get_commit_system_prompt(format, language).to_string();
|
||||
|
||||
if let Some(tmpl) = template {
|
||||
system_prompt.push_str(&format!(
|
||||
"\n\n## Commit Message Template\nFollow this template structure:\n{}",
|
||||
tmpl
|
||||
));
|
||||
}
|
||||
|
||||
// Add language instruction to the prompt
|
||||
let language_instruction = match language {
|
||||
Language::Chinese => "\n\n请用中文生成提交消息。",
|
||||
Language::Japanese => "\n\n日本語でコミットメッセージを生成してください。",
|
||||
Language::Korean => "\n\n한국어로 커밋 메시지를 생성하세요.",
|
||||
Language::Spanish => "\n\nPor favor, genera el mensaje de commit en español.",
|
||||
Language::French => "\n\nVeuillez générer le message de commit en français.",
|
||||
Language::German => "\n\nBitte generieren Sie die Commit-Nachricht auf Deutsch.",
|
||||
Language::English => "",
|
||||
};
|
||||
|
||||
let prompt = format!("{}{}", diff, language_instruction);
|
||||
let response = self
|
||||
.provider
|
||||
.generate_with_system(&system_prompt, &prompt)
|
||||
.await?;
|
||||
|
||||
parsing::parse_commit_response(&response, format)
|
||||
}
|
||||
|
||||
/// Generate tag message from commits
|
||||
pub async fn generate_tag_message(
|
||||
&self,
|
||||
version: &str,
|
||||
commits: &[String],
|
||||
language: Language,
|
||||
) -> Result<String> {
|
||||
let system_prompt = prompts::get_tag_system_prompt(language);
|
||||
let commits_text = commits.join("\n");
|
||||
|
||||
// Add language instruction to the prompt
|
||||
let language_instruction = match language {
|
||||
Language::Chinese => "\n\n请用中文生成标签消息。",
|
||||
Language::Japanese => "\n\n日本語でタグメッセージを生成してください。",
|
||||
Language::Korean => "\n\n한국어로 태그 메시지를 생성하세요.",
|
||||
Language::Spanish => "\n\nPor favor, genera el mensaje de etiqueta en español.",
|
||||
Language::French => "\n\nVeuillez générer le message de balise en français.",
|
||||
Language::German => "\n\nBitte generieren Sie die Tag-Nachricht auf Deutsch.",
|
||||
Language::English => "",
|
||||
};
|
||||
|
||||
let prompt = format!(
|
||||
"Version: {}\n\nCommits:\n{}{}",
|
||||
version, commits_text, language_instruction
|
||||
);
|
||||
|
||||
self.provider
|
||||
.generate_with_system(system_prompt, &prompt)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Generate changelog entry
|
||||
pub async fn generate_changelog_entry(
|
||||
&self,
|
||||
version: &str,
|
||||
commits: &[(String, String)], // (type, message)
|
||||
language: Language,
|
||||
) -> Result<String> {
|
||||
let system_prompt = prompts::get_changelog_system_prompt(language);
|
||||
|
||||
let commits_text = commits
|
||||
.iter()
|
||||
.map(|(t, m)| format!("- [{}] {}", t, m))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// Add language instruction to the prompt
|
||||
let language_instruction = match language {
|
||||
Language::Chinese => "\n\n请用中文生成变更日志。",
|
||||
Language::Japanese => "\n\n日本語で変更ログを生成してください。",
|
||||
Language::Korean => "\n\n한국어로 변경 로그를 생성하세요.",
|
||||
Language::Spanish => "\n\nPor favor, genera el registro de cambios en español.",
|
||||
Language::French => "\n\nVeuillez générer le journal des modifications en français.",
|
||||
Language::German => "\n\nBitte generieren Sie das Changelog auf Deutsch.",
|
||||
Language::English => "",
|
||||
};
|
||||
|
||||
let prompt = format!(
|
||||
"Version: {}\n\nCommits:\n{}{}",
|
||||
version, commits_text, language_instruction
|
||||
);
|
||||
|
||||
self.provider
|
||||
.generate_with_system(system_prompt, &prompt)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Check if provider is available
|
||||
pub async fn is_available(&self) -> bool {
|
||||
self.provider.is_available().await
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// HTTP client helper
|
||||
pub(crate) fn create_http_client(timeout: Duration) -> Result<reqwest::Client> {
|
||||
reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")
|
||||
}
|
||||
|
||||
|
||||
/// Test LLM connection
|
||||
pub async fn test_connection(manager: &crate::config::manager::ConfigManager) -> Result<String> {
|
||||
let client = crate::llm::rig::LlmClient::from_config(manager).await?;
|
||||
client.generate(None, "Say 'Hello, World!'").await
|
||||
}
|
||||
pub mod parsing;
|
||||
pub mod prompts;
|
||||
pub mod rig;
|
||||
pub mod thinking;
|
||||
|
||||
pub use parsing::GeneratedCommit;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Test LLM connection
|
||||
pub async fn test_connection(manager: &crate::config::manager::ConfigManager) -> Result<String> {
|
||||
let client = crate::llm::rig::LlmClient::from_config(manager).await?;
|
||||
client.generate(None, "Say 'Hello, World!'").await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user