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:
2026-08-17 16:04:26 +08:00
parent 16ffc94a06
commit 3c2b96a4d1
8 changed files with 14 additions and 3368 deletions

View File

@@ -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
}