From 760d564817124214170887557f1e1135bdde49b7 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Thu, 4 Dec 2025 11:23:13 +0800 Subject: [PATCH] fix(pricing): standardize model ID format for pricing lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normalize model IDs by removing vendor prefixes and converting dots to hyphens to ensure consistent pricing lookups across different API response formats. Changes: - Update seed data to use hyphen format (e.g., gpt-5-1, gemini-2-5-pro) - Add normalize_model_id() function to strip vendor prefixes (anthropic/, openai/) - Convert dots to hyphens in model IDs (claude-haiku-4.5 → claude-haiku-4-5) - Try both original and normalized IDs for exact matching - Use normalized ID for suffix-based fallback matching - Add comprehensive test cases for prefix and dot handling - Add warning log when no pricing found This ensures pricing lookups work correctly for: - Models with vendor prefixes: anthropic/claude-haiku-4.5 - Models with dots in version: claude-sonnet-4.5 - Models with date suffixes: claude-haiku-4-5-20240229 --- src-tauri/src/database/schema.rs | 11 +-- src-tauri/src/services/usage_stats.rs | 102 ++++++++++++++++++-------- 2 files changed, 76 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 1086ff0fe..047b34f01 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -559,6 +559,7 @@ impl Database { /// 插入默认模型定价数据 /// 格式: (model_id, display_name, input, output, cache_read, cache_creation) + /// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致 fn seed_model_pricing(conn: &Connection) -> Result<(), AppError> { let pricing_data = [ // Claude 4.5 系列 @@ -629,9 +630,9 @@ impl Database { "0.08", "1", ), - // GPT-5 系列 + // GPT-5 系列(model_id 使用短横线格式) ("gpt-5", "GPT-5", "1.25", "10", "0.125", "0"), - ("gpt-5.1", "GPT-5.1", "1.25", "10", "0.125", "0"), + ("gpt-5-1", "GPT-5.1", "1.25", "10", "0.125", "0"), // Gemini 3 系列 ( "gemini-3-pro-preview", @@ -641,9 +642,9 @@ impl Database { "0", "0", ), - // Gemini 2.5 系列 + // Gemini 2.5 系列(model_id 使用短横线格式) ( - "gemini-2.5-pro", + "gemini-2-5-pro", "Gemini 2.5 Pro", "1.25", "10", @@ -651,7 +652,7 @@ impl Database { "0", ), ( - "gemini-2.5-flash", + "gemini-2-5-flash", "Gemini 2.5 Flash", "0.3", "2.5", diff --git a/src-tauri/src/services/usage_stats.rs b/src-tauri/src/services/usage_stats.rs index 7f480579c..03ddba4e6 100644 --- a/src-tauri/src/services/usage_stats.rs +++ b/src-tauri/src/services/usage_stats.rs @@ -863,36 +863,59 @@ impl Database { } } +/// 标准化模型名称:去除供应商前缀并将点号替换为短横线 +/// 例如:anthropic/claude-haiku-4.5 → claude-haiku-4-5 +fn normalize_model_id(model_id: &str) -> String { + // 1. 去除供应商前缀(如 anthropic/、openai/) + let stripped = if let Some(pos) = model_id.find('/') { + &model_id[pos + 1..] + } else { + model_id + }; + // 2. 将点号替换为短横线(如 claude-haiku-4.5 → claude-haiku-4-5) + stripped.replace('.', "-") +} + pub(crate) fn find_model_pricing_row( conn: &Connection, model_id: &str, ) -> Result, AppError> { - // 1. 精确匹配 - let exact = conn - .query_row( - "SELECT input_cost_per_million, output_cost_per_million, - cache_read_cost_per_million, cache_creation_cost_per_million - FROM model_pricing - WHERE model_id = ?1", - [model_id], - |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - )) - }, - ) - .optional() - .map_err(|e| AppError::Database(format!("查询模型定价失败: {e}")))?; + // 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 + .query_row( + "SELECT input_cost_per_million, output_cost_per_million, + cache_read_cost_per_million, cache_creation_cost_per_million + FROM model_pricing + WHERE model_id = ?1", + [id], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + )) + }, + ) + .optional() + .map_err(|e| AppError::Database(format!("查询模型定价失败: {e}")))?; - if exact.is_some() { - return Ok(exact); + if exact.is_some() { + if id != model_id { + log::info!("模型 {model_id} 标准化后精确匹配到: {id}"); + } + return Ok(exact); + } } - // 2. 逐步删除后缀匹配(claude-sonnet-4-5-20250929 → claude-sonnet-4-5 → claude-sonnet-4 → claude-sonnet) - let mut current = model_id.to_string(); + // 2. 逐步删除后缀匹配(claude-haiku-4-5-20250929 → claude-haiku-4-5 → claude-haiku-4 → claude-haiku) + // 使用标准化后的名称进行后缀匹配 + let mut current = normalized; while let Some(pos) = current.rfind('-') { current = current[..pos].to_string(); @@ -921,6 +944,7 @@ pub(crate) fn find_model_pricing_row( } } + log::warn!("模型 {model_id} 未找到定价信息,成本将记录为 0"); Ok(None) } @@ -1002,28 +1026,42 @@ mod tests { let conn = lock_conn!(db.conn); // 测试精确匹配 - let result = find_model_pricing_row(&conn, "claude-4.1-sonnet")?; - assert!(result.is_some(), "应该能精确匹配 claude-4.1-sonnet"); + let result = find_model_pricing_row(&conn, "claude-sonnet-4-5")?; + assert!(result.is_some(), "应该能精确匹配 claude-sonnet-4-5"); - // 测试逐步删除后缀匹配 - 日期后缀 - let result = find_model_pricing_row(&conn, "claude-4.1-sonnet-20241022")?; + // 测试带供应商前缀的模型名称(anthropic/claude-haiku-4.5 → claude-haiku-4-5) + let result = find_model_pricing_row(&conn, "anthropic/claude-haiku-4.5")?; assert!( result.is_some(), - "应该能通过删除后缀匹配 claude-4.1-sonnet-20241022" + "应该能匹配带前缀的模型 anthropic/claude-haiku-4.5" + ); + + // 测试带供应商前缀 + 点号的模型名称 + let result = find_model_pricing_row(&conn, "anthropic/claude-sonnet-4.5")?; + assert!( + result.is_some(), + "应该能匹配带前缀的模型 anthropic/claude-sonnet-4.5" + ); + + // 测试逐步删除后缀匹配 - 日期后缀 + let result = find_model_pricing_row(&conn, "claude-sonnet-4-5-20241022")?; + assert!( + result.is_some(), + "应该能通过删除后缀匹配 claude-sonnet-4-5-20241022" ); // 测试逐步删除后缀匹配 - 多个后缀 - let result = find_model_pricing_row(&conn, "claude-4.5-haiku-20240229-preview")?; + let result = find_model_pricing_row(&conn, "claude-haiku-4-5-20240229-preview")?; assert!( result.is_some(), - "应该能通过删除后缀匹配 claude-4.5-haiku-20240229-preview" + "应该能通过删除后缀匹配 claude-haiku-4-5-20240229-preview" ); // 测试 GPT 模型 - let result = find_model_pricing_row(&conn, "gpt-5.0-2024-11-20")?; + let result = find_model_pricing_row(&conn, "gpt-5-2024-11-20")?; assert!( result.is_some(), - "应该能通过删除后缀匹配 gpt-5.0-2024-11-20" + "应该能通过删除后缀匹配 gpt-5-2024-11-20" ); // 测试 Gemini 模型