mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +08:00
feat(services): add pagination and enhanced filtering for request logs
Usage stats service: - Change get_request_logs() from limit/offset to page/page_size pagination - Return PaginatedLogs with total count, page, and page_size - Add appType and providerName filters with LIKE search - Add is_streaming, first_token_ms, duration_ms to RequestLogDetail - Join with providers table for provider name lookup Commands: - Update get_request_logs command signature for pagination params Module exports: - Export PaginatedLogs struct
This commit is contained in:
@@ -41,10 +41,10 @@ pub fn get_model_stats(state: State<'_, AppState>) -> Result<Vec<ModelStats>, Ap
|
|||||||
pub fn get_request_logs(
|
pub fn get_request_logs(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
filters: LogFilters,
|
filters: LogFilters,
|
||||||
limit: u32,
|
page: u32,
|
||||||
offset: u32,
|
page_size: u32,
|
||||||
) -> Result<Vec<RequestLogDetail>, AppError> {
|
) -> Result<PaginatedLogs, AppError> {
|
||||||
state.db.get_request_logs(&filters, limit, offset)
|
state.db.get_request_logs(&filters, page, page_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取单个请求详情
|
/// 获取单个请求详情
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ pub use skill::{Skill, SkillRepo, SkillService};
|
|||||||
pub use speedtest::{EndpointLatency, SpeedtestService};
|
pub use speedtest::{EndpointLatency, SpeedtestService};
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use usage_stats::{
|
pub use usage_stats::{
|
||||||
DailyStats, LogFilters, ModelStats, ProviderLimitStatus, ProviderStats, RequestLogDetail,
|
DailyStats, LogFilters, ModelStats, PaginatedLogs, ProviderLimitStatus, ProviderStats,
|
||||||
UsageSummary,
|
RequestLogDetail, UsageSummary,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
use crate::database::{lock_conn, Database};
|
use crate::database::{lock_conn, Database};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use regex::Regex;
|
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -67,13 +66,24 @@ pub struct ModelStats {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LogFilters {
|
pub struct LogFilters {
|
||||||
pub provider_id: Option<String>,
|
pub app_type: Option<String>,
|
||||||
|
pub provider_name: Option<String>,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
pub status_code: Option<u16>,
|
pub status_code: Option<u16>,
|
||||||
pub start_date: Option<i64>,
|
pub start_date: Option<i64>,
|
||||||
pub end_date: Option<i64>,
|
pub end_date: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 分页请求日志响应
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PaginatedLogs {
|
||||||
|
pub data: Vec<RequestLogDetail>,
|
||||||
|
pub total: u32,
|
||||||
|
pub page: u32,
|
||||||
|
pub page_size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// 请求日志详情
|
/// 请求日志详情
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -93,7 +103,10 @@ pub struct RequestLogDetail {
|
|||||||
pub cache_read_cost_usd: String,
|
pub cache_read_cost_usd: String,
|
||||||
pub cache_creation_cost_usd: String,
|
pub cache_creation_cost_usd: String,
|
||||||
pub total_cost_usd: String,
|
pub total_cost_usd: String,
|
||||||
|
pub is_streaming: bool,
|
||||||
pub latency_ms: u64,
|
pub latency_ms: u64,
|
||||||
|
pub first_token_ms: Option<u64>,
|
||||||
|
pub duration_ms: Option<u64>,
|
||||||
pub status_code: u16,
|
pub status_code: u16,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
@@ -381,36 +394,40 @@ impl Database {
|
|||||||
Ok(stats)
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取请求日志列表
|
/// 获取请求日志列表(分页)
|
||||||
pub fn get_request_logs(
|
pub fn get_request_logs(
|
||||||
&self,
|
&self,
|
||||||
filters: &LogFilters,
|
filters: &LogFilters,
|
||||||
limit: u32,
|
page: u32,
|
||||||
offset: u32,
|
page_size: u32,
|
||||||
) -> Result<Vec<RequestLogDetail>, AppError> {
|
) -> Result<PaginatedLogs, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
let mut conditions = Vec::new();
|
let mut conditions = Vec::new();
|
||||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||||
|
|
||||||
if let Some(ref provider_id) = filters.provider_id {
|
if let Some(ref app_type) = filters.app_type {
|
||||||
conditions.push("provider_id = ?");
|
conditions.push("l.app_type = ?");
|
||||||
params.push(Box::new(provider_id.clone()));
|
params.push(Box::new(app_type.clone()));
|
||||||
|
}
|
||||||
|
if let Some(ref provider_name) = filters.provider_name {
|
||||||
|
conditions.push("p.name LIKE ?");
|
||||||
|
params.push(Box::new(format!("%{provider_name}%")));
|
||||||
}
|
}
|
||||||
if let Some(ref model) = filters.model {
|
if let Some(ref model) = filters.model {
|
||||||
conditions.push("model = ?");
|
conditions.push("l.model LIKE ?");
|
||||||
params.push(Box::new(model.clone()));
|
params.push(Box::new(format!("%{model}%")));
|
||||||
}
|
}
|
||||||
if let Some(status) = filters.status_code {
|
if let Some(status) = filters.status_code {
|
||||||
conditions.push("status_code = ?");
|
conditions.push("l.status_code = ?");
|
||||||
params.push(Box::new(status as i64));
|
params.push(Box::new(status as i64));
|
||||||
}
|
}
|
||||||
if let Some(start) = filters.start_date {
|
if let Some(start) = filters.start_date {
|
||||||
conditions.push("created_at >= ?");
|
conditions.push("l.created_at >= ?");
|
||||||
params.push(Box::new(start));
|
params.push(Box::new(start));
|
||||||
}
|
}
|
||||||
if let Some(end) = filters.end_date {
|
if let Some(end) = filters.end_date {
|
||||||
conditions.push("created_at <= ?");
|
conditions.push("l.created_at <= ?");
|
||||||
params.push(Box::new(end));
|
params.push(Box::new(end));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,14 +437,28 @@ impl Database {
|
|||||||
format!("WHERE {}", conditions.join(" AND "))
|
format!("WHERE {}", conditions.join(" AND "))
|
||||||
};
|
};
|
||||||
|
|
||||||
params.push(Box::new(limit as i64));
|
// 获取总数
|
||||||
|
let count_sql = format!(
|
||||||
|
"SELECT COUNT(*) FROM proxy_request_logs l
|
||||||
|
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
||||||
|
{where_clause}"
|
||||||
|
);
|
||||||
|
let count_params: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
|
||||||
|
let total: u32 = conn.query_row(&count_sql, count_params.as_slice(), |row| {
|
||||||
|
row.get::<_, i64>(0).map(|v| v as u32)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
let offset = page * page_size;
|
||||||
|
params.push(Box::new(page_size as i64));
|
||||||
params.push(Box::new(offset as i64));
|
params.push(Box::new(offset as i64));
|
||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT l.request_id, l.provider_id, p.name as provider_name, l.app_type, l.model,
|
"SELECT l.request_id, l.provider_id, p.name as provider_name, l.app_type, l.model,
|
||||||
l.input_tokens, l.output_tokens, l.cache_read_tokens, l.cache_creation_tokens,
|
l.input_tokens, l.output_tokens, l.cache_read_tokens, l.cache_creation_tokens,
|
||||||
l.input_cost_usd, l.output_cost_usd, l.cache_read_cost_usd, l.cache_creation_cost_usd, l.total_cost_usd,
|
l.input_cost_usd, l.output_cost_usd, l.cache_read_cost_usd, l.cache_creation_cost_usd, l.total_cost_usd,
|
||||||
l.latency_ms, l.status_code, l.error_message, l.created_at
|
l.is_streaming, l.latency_ms, l.first_token_ms, l.duration_ms,
|
||||||
|
l.status_code, l.error_message, l.created_at
|
||||||
FROM proxy_request_logs l
|
FROM proxy_request_logs l
|
||||||
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
||||||
{where_clause}
|
{where_clause}
|
||||||
@@ -453,10 +484,13 @@ impl Database {
|
|||||||
cache_read_cost_usd: row.get(11)?,
|
cache_read_cost_usd: row.get(11)?,
|
||||||
cache_creation_cost_usd: row.get(12)?,
|
cache_creation_cost_usd: row.get(12)?,
|
||||||
total_cost_usd: row.get(13)?,
|
total_cost_usd: row.get(13)?,
|
||||||
latency_ms: row.get::<_, i64>(14)? as u64,
|
is_streaming: row.get::<_, i64>(14)? != 0,
|
||||||
status_code: row.get::<_, i64>(15)? as u16,
|
latency_ms: row.get::<_, i64>(15)? as u64,
|
||||||
error_message: row.get(16)?,
|
first_token_ms: row.get::<_, Option<i64>>(16)?.map(|v| v as u64),
|
||||||
created_at: row.get(17)?,
|
duration_ms: row.get::<_, Option<i64>>(17)?.map(|v| v as u64),
|
||||||
|
status_code: row.get::<_, i64>(18)? as u16,
|
||||||
|
error_message: row.get(19)?,
|
||||||
|
created_at: row.get(20)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -475,7 +509,12 @@ impl Database {
|
|||||||
logs.push(log);
|
logs.push(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(logs)
|
Ok(PaginatedLogs {
|
||||||
|
data: logs,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取单个请求详情
|
/// 获取单个请求详情
|
||||||
@@ -489,7 +528,8 @@ impl Database {
|
|||||||
"SELECT l.request_id, l.provider_id, p.name as provider_name, l.app_type, l.model,
|
"SELECT l.request_id, l.provider_id, p.name as provider_name, l.app_type, l.model,
|
||||||
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
|
||||||
input_cost_usd, output_cost_usd, cache_read_cost_usd, cache_creation_cost_usd, total_cost_usd,
|
input_cost_usd, output_cost_usd, cache_read_cost_usd, cache_creation_cost_usd, total_cost_usd,
|
||||||
latency_ms, status_code, error_message, created_at
|
is_streaming, latency_ms, first_token_ms, duration_ms,
|
||||||
|
status_code, error_message, created_at
|
||||||
FROM proxy_request_logs l
|
FROM proxy_request_logs l
|
||||||
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
LEFT JOIN providers p ON l.provider_id = p.id AND l.app_type = p.app_type
|
||||||
WHERE l.request_id = ?",
|
WHERE l.request_id = ?",
|
||||||
@@ -510,10 +550,13 @@ impl Database {
|
|||||||
cache_read_cost_usd: row.get(11)?,
|
cache_read_cost_usd: row.get(11)?,
|
||||||
cache_creation_cost_usd: row.get(12)?,
|
cache_creation_cost_usd: row.get(12)?,
|
||||||
total_cost_usd: row.get(13)?,
|
total_cost_usd: row.get(13)?,
|
||||||
latency_ms: row.get::<_, i64>(14)? as u64,
|
is_streaming: row.get::<_, i64>(14)? != 0,
|
||||||
status_code: row.get::<_, i64>(15)? as u16,
|
latency_ms: row.get::<_, i64>(15)? as u64,
|
||||||
error_message: row.get(16)?,
|
first_token_ms: row.get::<_, Option<i64>>(16)?.map(|v| v as u64),
|
||||||
created_at: row.get(17)?,
|
duration_ms: row.get::<_, Option<i64>>(17)?.map(|v| v as u64),
|
||||||
|
status_code: row.get::<_, i64>(18)? as u16,
|
||||||
|
error_message: row.get(19)?,
|
||||||
|
created_at: row.get(20)?,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -609,6 +652,56 @@ impl Database {
|
|||||||
monthly_exceeded,
|
monthly_exceeded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新每日统计聚合
|
||||||
|
///
|
||||||
|
/// 在请求完成后调用,更新 usage_daily_stats 表
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn update_daily_stats(
|
||||||
|
&self,
|
||||||
|
provider_id: &str,
|
||||||
|
app_type: &str,
|
||||||
|
model: &str,
|
||||||
|
input_tokens: u32,
|
||||||
|
output_tokens: u32,
|
||||||
|
total_cost: &str,
|
||||||
|
is_success: bool,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let conn = lock_conn!(self.conn);
|
||||||
|
let date = Utc::now().format("%Y-%m-%d").to_string();
|
||||||
|
|
||||||
|
// 使用 UPSERT 更新或插入统计
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO usage_daily_stats (
|
||||||
|
date, provider_id, app_type, model,
|
||||||
|
request_count, total_input_tokens, total_output_tokens,
|
||||||
|
total_cost_usd, success_count, error_count
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, 1, ?5, ?6, ?7, ?8, ?9)
|
||||||
|
ON CONFLICT(date, provider_id, app_type, model) DO UPDATE SET
|
||||||
|
request_count = request_count + 1,
|
||||||
|
total_input_tokens = total_input_tokens + ?5,
|
||||||
|
total_output_tokens = total_output_tokens + ?6,
|
||||||
|
total_cost_usd = CAST(
|
||||||
|
CAST(total_cost_usd AS REAL) + CAST(?7 AS REAL) AS TEXT
|
||||||
|
),
|
||||||
|
success_count = success_count + ?8,
|
||||||
|
error_count = error_count + ?9",
|
||||||
|
params![
|
||||||
|
date,
|
||||||
|
provider_id,
|
||||||
|
app_type,
|
||||||
|
model,
|
||||||
|
input_tokens,
|
||||||
|
output_tokens,
|
||||||
|
total_cost,
|
||||||
|
if is_success { 1 } else { 0 },
|
||||||
|
if is_success { 0 } else { 1 },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|e| AppError::Database(format!("更新每日统计失败: {e}")))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider 限额状态
|
/// Provider 限额状态
|
||||||
@@ -774,6 +867,7 @@ pub(crate) fn find_model_pricing_row(
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<Option<(String, String, String, String)>, AppError> {
|
) -> Result<Option<(String, String, String, String)>, AppError> {
|
||||||
|
// 1. 精确匹配
|
||||||
let exact = conn
|
let exact = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT input_cost_per_million, output_cost_per_million,
|
"SELECT input_cost_per_million, output_cost_per_million,
|
||||||
@@ -797,59 +891,39 @@ pub(crate) fn find_model_pricing_row(
|
|||||||
return Ok(exact);
|
return Ok(exact);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stmt = conn.prepare(
|
// 2. 逐步删除后缀匹配(claude-sonnet-4-5-20250929 → claude-sonnet-4-5 → claude-sonnet-4 → claude-sonnet)
|
||||||
"SELECT model_id, input_cost_per_million, output_cost_per_million,
|
let mut current = model_id.to_string();
|
||||||
cache_read_cost_per_million, cache_creation_cost_per_million
|
while let Some(pos) = current.rfind('-') {
|
||||||
FROM model_pricing
|
current = current[..pos].to_string();
|
||||||
WHERE model_id LIKE '%*%' OR model_id LIKE '%?%'
|
|
||||||
ORDER BY LENGTH(model_id) DESC",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let rows = stmt
|
let result = conn
|
||||||
.query_map([], |row| {
|
.query_row(
|
||||||
Ok((
|
"SELECT input_cost_per_million, output_cost_per_million,
|
||||||
row.get::<_, String>(0)?,
|
cache_read_cost_per_million, cache_creation_cost_per_million
|
||||||
row.get::<_, String>(1)?,
|
FROM model_pricing
|
||||||
row.get::<_, String>(2)?,
|
WHERE model_id = ?1",
|
||||||
row.get::<_, String>(3)?,
|
[¤t],
|
||||||
row.get::<_, String>(4)?,
|
|row| {
|
||||||
))
|
Ok((
|
||||||
})?
|
row.get::<_, String>(0)?,
|
||||||
.filter_map(|res| res.ok());
|
row.get::<_, String>(1)?,
|
||||||
|
row.get::<_, String>(2)?,
|
||||||
|
row.get::<_, String>(3)?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.map_err(|e| AppError::Database(format!("查询模型定价失败: {e}")))?;
|
||||||
|
|
||||||
for (pattern, input, output, cache_read, cache_creation) in rows {
|
if result.is_some() {
|
||||||
if matches_model_pattern(&pattern, model_id) {
|
log::info!("模型 {model_id} 通过删除后缀匹配到: {current}");
|
||||||
return Ok(Some((input, output, cache_read, cache_creation)));
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches_model_pattern(pattern: &str, model_id: &str) -> bool {
|
|
||||||
if pattern == model_id {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pattern.contains('*') && !pattern.contains('?') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut regex_pattern = String::from("^");
|
|
||||||
for ch in pattern.chars() {
|
|
||||||
match ch {
|
|
||||||
'*' => regex_pattern.push_str(".*"),
|
|
||||||
'?' => regex_pattern.push('.'),
|
|
||||||
_ => regex_pattern.push_str(®ex::escape(&ch.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
regex_pattern.push('$');
|
|
||||||
|
|
||||||
Regex::new(®ex_pattern)
|
|
||||||
.map(|re| re.is_match(model_id))
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -921,4 +995,55 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_model_pricing_matching() -> Result<(), AppError> {
|
||||||
|
let db = Database::memory()?;
|
||||||
|
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-4.1-sonnet-20241022")?;
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"应该能通过删除后缀匹配 claude-4.1-sonnet-20241022"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试逐步删除后缀匹配 - 多个后缀
|
||||||
|
let result = find_model_pricing_row(&conn, "claude-4.5-haiku-20240229-preview")?;
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"应该能通过删除后缀匹配 claude-4.5-haiku-20240229-preview"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试 GPT 模型
|
||||||
|
let result = find_model_pricing_row(&conn, "gpt-5.0-2024-11-20")?;
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"应该能通过删除后缀匹配 gpt-5.0-2024-11-20"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试 Gemini 模型
|
||||||
|
let result = find_model_pricing_row(&conn, "gemini-2.5-flash-exp")?;
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"应该能通过删除后缀匹配 gemini-2.5-flash-exp"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试 claude-sonnet-4-5 命名格式
|
||||||
|
let result = find_model_pricing_row(&conn, "claude-sonnet-4-5-20250929")?;
|
||||||
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"应该能通过删除后缀匹配 claude-sonnet-4-5-20250929"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试不存在的模型
|
||||||
|
let result = find_model_pricing_row(&conn, "unknown-model-123")?;
|
||||||
|
assert!(result.is_none(), "不应该匹配不存在的模型");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user