fix(pricing): standardize model ID format for pricing lookup

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
This commit is contained in:
YoVinchen
2025-12-04 11:23:13 +08:00
parent 1121347a45
commit 760d564817
2 changed files with 76 additions and 37 deletions
+6 -5
View File
@@ -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",
+70 -32
View File
@@ -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<Option<(String, String, String, String)>, 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 模型