diff --git a/src-tauri/src/commands/model_test.rs b/src-tauri/src/commands/model_test.rs index 04b50bb9f..a751875f2 100644 --- a/src-tauri/src/commands/model_test.rs +++ b/src-tauri/src/commands/model_test.rs @@ -2,7 +2,9 @@ use crate::app_config::AppType; use crate::error::AppError; -use crate::services::model_test::{ModelTestConfig, ModelTestLog, ModelTestResult, ModelTestService}; +use crate::services::model_test::{ + ModelTestConfig, ModelTestLog, ModelTestResult, ModelTestService, +}; use crate::store::AppState; use tauri::State; @@ -20,7 +22,7 @@ pub async fn test_provider_model( let providers = state.db.get_all_providers(app_type.as_str())?; let provider = providers .get(&provider_id) - .ok_or_else(|| AppError::Message(format!("供应商 {} 不存在", provider_id)))?; + .ok_or_else(|| AppError::Message(format!("供应商 {provider_id} 不存在")))?; // 执行测试 let result = ModelTestService::test_provider(&app_type, provider, &config).await?; diff --git a/src-tauri/src/proxy/usage/parser.rs b/src-tauri/src/proxy/usage/parser.rs index 1aa99ae1e..950d6f669 100644 --- a/src-tauri/src/proxy/usage/parser.rs +++ b/src-tauri/src/proxy/usage/parser.rs @@ -60,7 +60,9 @@ impl TokenUsage { "message_start" => { if let Some(msg_usage) = event.get("message").and_then(|m| m.get("usage")) { // 从 message_start 获取 input_tokens(原生 Claude API) - if let Some(input) = msg_usage.get("input_tokens").and_then(|v| v.as_u64()) { + if let Some(input) = + msg_usage.get("input_tokens").and_then(|v| v.as_u64()) + { usage.input_tokens = input as u32; } usage.cache_read_tokens = msg_usage @@ -78,13 +80,17 @@ impl TokenUsage { "message_delta" => { if let Some(delta_usage) = event.get("usage") { // 从 message_delta 获取 output_tokens - if let Some(output) = delta_usage.get("output_tokens").and_then(|v| v.as_u64()) { + if let Some(output) = + delta_usage.get("output_tokens").and_then(|v| v.as_u64()) + { usage.output_tokens = output as u32; } // OpenRouter 转换后的流式响应:input_tokens 也在 message_delta 中 // 如果 message_start 中没有 input_tokens,则从 message_delta 获取 if usage.input_tokens == 0 { - if let Some(input) = delta_usage.get("input_tokens").and_then(|v| v.as_u64()) { + if let Some(input) = + delta_usage.get("input_tokens").and_then(|v| v.as_u64()) + { usage.input_tokens = input as u32; } } diff --git a/src-tauri/src/services/model_test.rs b/src-tauri/src/services/model_test.rs index 1a5eefe75..a62dacf7b 100644 --- a/src-tauri/src/services/model_test.rs +++ b/src-tauri/src/services/model_test.rs @@ -98,16 +98,34 @@ impl ModelTestService { let result = match app_type { AppType::Claude => { - Self::test_claude(&client, provider, adapter.as_ref(), model, &config.test_prompt) - .await + Self::test_claude( + &client, + provider, + adapter.as_ref(), + model, + &config.test_prompt, + ) + .await } AppType::Codex => { - Self::test_codex(&client, provider, adapter.as_ref(), model, &config.test_prompt) - .await + Self::test_codex( + &client, + provider, + adapter.as_ref(), + model, + &config.test_prompt, + ) + .await } AppType::Gemini => { - Self::test_gemini(&client, provider, adapter.as_ref(), model, &config.test_prompt) - .await + Self::test_gemini( + &client, + provider, + adapter.as_ref(), + model, + &config.test_prompt, + ) + .await } }; @@ -142,20 +160,20 @@ impl ModelTestService { model: &str, prompt: &str, ) -> Result<(u16, String), AppError> { - let base_url = adapter.extract_base_url(provider).map_err(|e| { - AppError::Message(format!("提取 base_url 失败: {e}")) - })?; + let base_url = adapter + .extract_base_url(provider) + .map_err(|e| AppError::Message(format!("提取 base_url 失败: {e}")))?; - let auth = adapter.extract_auth(provider).ok_or_else(|| { - AppError::Message("未找到 API Key".to_string()) - })?; + let auth = adapter + .extract_auth(provider) + .ok_or_else(|| AppError::Message("未找到 API Key".to_string()))?; // 智能拼接 URL,避免重复 /v1 let base = base_url.trim_end_matches('/'); let url = if base.ends_with("/v1") { - format!("{}/messages", base) + format!("{base}/messages") } else { - format!("{}/v1/messages", base) + format!("{base}/v1/messages") }; let body = json!({ @@ -185,19 +203,22 @@ impl ModelTestService { if response.status().is_success() { // 先获取文本,再尝试解析 JSON(兼容流式响应) let text = response.text().await.unwrap_or_default(); - + // 尝试解析 JSON if let Ok(data) = serde_json::from_str::(&text) { - if data.get("type").is_some() || data.get("content").is_some() || data.get("id").is_some() { + if data.get("type").is_some() + || data.get("content").is_some() + || data.get("id").is_some() + { return Ok((status, "模型测试成功".to_string())); } } - + // 即使无法解析 JSON,只要状态码是 200 就认为成功 Ok((status, "模型测试成功".to_string())) } else { let error_text = response.text().await.unwrap_or_default(); - Err(AppError::Message(format!("HTTP {}: {}", status, error_text))) + Err(AppError::Message(format!("HTTP {status}: {error_text}"))) } } @@ -209,20 +230,20 @@ impl ModelTestService { model: &str, prompt: &str, ) -> Result<(u16, String), AppError> { - let base_url = adapter.extract_base_url(provider).map_err(|e| { - AppError::Message(format!("提取 base_url 失败: {e}")) - })?; + let base_url = adapter + .extract_base_url(provider) + .map_err(|e| AppError::Message(format!("提取 base_url 失败: {e}")))?; - let auth = adapter.extract_auth(provider).ok_or_else(|| { - AppError::Message("未找到 API Key".to_string()) - })?; + let auth = adapter + .extract_auth(provider) + .ok_or_else(|| AppError::Message("未找到 API Key".to_string()))?; // 智能拼接 URL,避免重复 /v1 let base = base_url.trim_end_matches('/'); let url = if base.ends_with("/v1") { - format!("{}/chat/completions", base) + format!("{base}/chat/completions") } else { - format!("{}/v1/chat/completions", base) + format!("{base}/v1/chat/completions") }; let body = json!({ @@ -256,18 +277,18 @@ impl ModelTestService { if response.status().is_success() { // 先获取文本,再尝试解析 JSON let text = response.text().await.unwrap_or_default(); - + if let Ok(data) = serde_json::from_str::(&text) { if data.get("choices").is_some() || data.get("id").is_some() { return Ok((status, "模型测试成功".to_string())); } } - + // 即使无法解析 JSON,只要状态码是 200 就认为成功 Ok((status, "模型测试成功".to_string())) } else { let error_text = response.text().await.unwrap_or_default(); - Err(AppError::Message(format!("HTTP {}: {}", status, error_text))) + Err(AppError::Message(format!("HTTP {status}: {error_text}"))) } } @@ -279,13 +300,13 @@ impl ModelTestService { model: &str, prompt: &str, ) -> Result<(u16, String), AppError> { - let base_url = adapter.extract_base_url(provider).map_err(|e| { - AppError::Message(format!("提取 base_url 失败: {e}")) - })?; + let base_url = adapter + .extract_base_url(provider) + .map_err(|e| AppError::Message(format!("提取 base_url 失败: {e}")))?; - let auth = adapter.extract_auth(provider).ok_or_else(|| { - AppError::Message("未找到 API Key".to_string()) - })?; + let auth = adapter + .extract_auth(provider) + .ok_or_else(|| AppError::Message("未找到 API Key".to_string()))?; let url = format!( "{}/v1beta/models/{}:generateContent?key={}", @@ -323,9 +344,10 @@ impl ModelTestService { let status = response.status().as_u16(); if response.status().is_success() { - let data: Value = response.json().await.map_err(|e| { - AppError::Message(format!("解析响应失败: {e}")) - })?; + let data: Value = response + .json() + .await + .map_err(|e| AppError::Message(format!("解析响应失败: {e}")))?; if data.get("candidates").is_some() { Ok((status, "模型测试成功".to_string())) @@ -334,12 +356,15 @@ impl ModelTestService { } } else { let error_text = response.text().await.unwrap_or_default(); - Err(AppError::Message(format!("HTTP {}: {}", status, error_text))) + Err(AppError::Message(format!("HTTP {status}: {error_text}"))) } } /// 添加 Claude 认证头 - fn add_claude_auth(request: reqwest::RequestBuilder, auth: &AuthInfo) -> reqwest::RequestBuilder { + fn add_claude_auth( + request: reqwest::RequestBuilder, + auth: &AuthInfo, + ) -> reqwest::RequestBuilder { request .header("x-api-key", &auth.api_key) .header("anthropic-version", "2023-06-01") @@ -360,9 +385,10 @@ impl Database { prompt: &str, result: &ModelTestResult, ) -> Result { - let conn = self.conn.lock().map_err(|e| { - AppError::Database(format!("获取数据库连接失败: {e}")) - })?; + let conn = self + .conn + .lock() + .map_err(|e| AppError::Database(format!("获取数据库连接失败: {e}")))?; conn.execute( "INSERT INTO model_test_logs @@ -393,9 +419,10 @@ impl Database { provider_id: Option<&str>, limit: u32, ) -> Result, AppError> { - let conn = self.conn.lock().map_err(|e| { - AppError::Database(format!("获取数据库连接失败: {e}")) - })?; + let conn = self + .conn + .lock() + .map_err(|e| AppError::Database(format!("获取数据库连接失败: {e}")))?; let mut sql = String::from( "SELECT id, provider_id, provider_name, app_type, model, prompt, success, message, response_time_ms, http_status, tested_at @@ -419,7 +446,9 @@ impl Database { let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - let mut stmt = conn.prepare(&sql).map_err(|e| AppError::Database(e.to_string()))?; + let mut stmt = conn + .prepare(&sql) + .map_err(|e| AppError::Database(e.to_string()))?; let logs = stmt .query_map(params_refs.as_slice(), |row| { @@ -447,34 +476,34 @@ impl Database { /// 获取模型测试配置 pub fn get_model_test_config(&self) -> Result { match self.get_setting("model_test_config")? { - Some(json) => serde_json::from_str(&json).map_err(|e| { - AppError::Message(format!("解析模型测试配置失败: {e}")) - }), + Some(json) => serde_json::from_str(&json) + .map_err(|e| AppError::Message(format!("解析模型测试配置失败: {e}"))), None => Ok(ModelTestConfig::default()), } } /// 保存模型测试配置 pub fn save_model_test_config(&self, config: &ModelTestConfig) -> Result<(), AppError> { - let json = serde_json::to_string(config).map_err(|e| { - AppError::Message(format!("序列化模型测试配置失败: {e}")) - })?; + let json = serde_json::to_string(config) + .map_err(|e| AppError::Message(format!("序列化模型测试配置失败: {e}")))?; self.set_setting("model_test_config", &json) } /// 清理旧的测试日志(保留最近 N 条) pub fn cleanup_model_test_logs(&self, keep_count: u32) -> Result { - let conn = self.conn.lock().map_err(|e| { - AppError::Database(format!("获取数据库连接失败: {e}")) - })?; + let conn = self + .conn + .lock() + .map_err(|e| AppError::Database(format!("获取数据库连接失败: {e}")))?; - let deleted = conn.execute( - "DELETE FROM model_test_logs WHERE id NOT IN ( + let deleted = conn + .execute( + "DELETE FROM model_test_logs WHERE id NOT IN ( SELECT id FROM model_test_logs ORDER BY tested_at DESC LIMIT ? )", - rusqlite::params![keep_count as i64], - ) - .map_err(|e| AppError::Database(e.to_string()))?; + rusqlite::params![keep_count as i64], + ) + .map_err(|e| AppError::Database(e.to_string()))?; Ok(deleted as u64) } diff --git a/src-tauri/src/services/usage_stats.rs b/src-tauri/src/services/usage_stats.rs index 03ddba4e6..d679d7f19 100644 --- a/src-tauri/src/services/usage_stats.rs +++ b/src-tauri/src/services/usage_stats.rs @@ -883,7 +883,7 @@ pub(crate) fn find_model_pricing_row( // 0. 标准化模型名称(去除前缀 + 点号转短横线) // 例如:anthropic/claude-haiku-4.5 → claude-haiku-4-5 let normalized = normalize_model_id(model_id); - + // 1. 精确匹配(先尝试原始名称,再尝试标准化后的名称) for id in [model_id, normalized.as_str()] { let exact = conn @@ -1059,10 +1059,7 @@ mod tests { // 测试 GPT 模型 let result = find_model_pricing_row(&conn, "gpt-5-2024-11-20")?; - assert!( - result.is_some(), - "应该能通过删除后缀匹配 gpt-5-2024-11-20" - ); + assert!(result.is_some(), "应该能通过删除后缀匹配 gpt-5-2024-11-20"); // 测试 Gemini 模型 let result = find_model_pricing_row(&conn, "gemini-2.5-flash-exp")?;