mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 04:02:02 +08:00
feat(db): add usage tracking schema and types
Add database tables for proxy request logs and model pricing. Extend Provider and error types to support usage statistics.
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
//! Usage Logger - 记录 API 请求使用情况
|
||||
|
||||
use super::calculator::{CostBreakdown, CostCalculator, ModelPricing};
|
||||
use super::parser::TokenUsage;
|
||||
use crate::database::Database;
|
||||
use crate::error::AppError;
|
||||
use rust_decimal::Decimal;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// 请求日志
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestLog {
|
||||
pub request_id: String,
|
||||
pub provider_id: String,
|
||||
pub app_type: String,
|
||||
pub model: String,
|
||||
pub usage: TokenUsage,
|
||||
pub cost: Option<CostBreakdown>,
|
||||
pub latency_ms: u64,
|
||||
pub status_code: u16,
|
||||
pub error_message: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 使用量记录器
|
||||
pub struct UsageLogger<'a> {
|
||||
db: &'a Database,
|
||||
}
|
||||
|
||||
impl<'a> UsageLogger<'a> {
|
||||
pub fn new(db: &'a Database) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// 记录成功的请求
|
||||
pub fn log_request(&self, log: &RequestLog) -> Result<(), AppError> {
|
||||
let conn = crate::database::lock_conn!(self.db.conn);
|
||||
|
||||
let (input_cost, output_cost, cache_read_cost, cache_creation_cost, total_cost) =
|
||||
if let Some(cost) = &log.cost {
|
||||
(
|
||||
cost.input_cost.to_string(),
|
||||
cost.output_cost.to_string(),
|
||||
cost.cache_read_cost.to_string(),
|
||||
cost.cache_creation_cost.to_string(),
|
||||
cost.total_cost.to_string(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"0".to_string(),
|
||||
"0".to_string(),
|
||||
"0".to_string(),
|
||||
"0".to_string(),
|
||||
"0".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let created_at = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO proxy_request_logs (
|
||||
request_id, provider_id, app_type, model,
|
||||
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,
|
||||
latency_ms, status_code, error_message, session_id, created_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
|
||||
rusqlite::params![
|
||||
log.request_id,
|
||||
log.provider_id,
|
||||
log.app_type,
|
||||
log.model,
|
||||
log.usage.input_tokens,
|
||||
log.usage.output_tokens,
|
||||
log.usage.cache_read_tokens,
|
||||
log.usage.cache_creation_tokens,
|
||||
input_cost,
|
||||
output_cost,
|
||||
cache_read_cost,
|
||||
cache_creation_cost,
|
||||
total_cost,
|
||||
log.latency_ms as i64,
|
||||
log.status_code as i64,
|
||||
log.error_message,
|
||||
log.session_id,
|
||||
created_at,
|
||||
],
|
||||
)
|
||||
.map_err(|e| AppError::Database(format!("记录请求日志失败: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录失败的请求
|
||||
pub fn log_error(
|
||||
&self,
|
||||
request_id: String,
|
||||
provider_id: String,
|
||||
app_type: String,
|
||||
model: String,
|
||||
status_code: u16,
|
||||
error_message: String,
|
||||
latency_ms: u64,
|
||||
) -> Result<(), AppError> {
|
||||
let log = RequestLog {
|
||||
request_id,
|
||||
provider_id,
|
||||
app_type,
|
||||
model,
|
||||
usage: TokenUsage::default(),
|
||||
cost: None,
|
||||
latency_ms,
|
||||
status_code,
|
||||
error_message: Some(error_message),
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
self.log_request(&log)
|
||||
}
|
||||
|
||||
/// 获取模型定价
|
||||
pub fn get_model_pricing(&self, model_id: &str) -> Result<Option<ModelPricing>, AppError> {
|
||||
let conn = crate::database::lock_conn!(self.db.conn);
|
||||
|
||||
let result = 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(ModelPricing::from_strings(
|
||||
&row.get::<_, String>(0)?,
|
||||
&row.get::<_, String>(1)?,
|
||||
&row.get::<_, String>(2)?,
|
||||
&row.get::<_, String>(3)?,
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(Ok(pricing)) => Ok(Some(pricing)),
|
||||
Ok(Err(e)) => Err(AppError::Database(format!("解析定价数据失败: {e}"))),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(AppError::Database(format!("查询定价失败: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算并记录请求
|
||||
pub fn log_with_calculation(
|
||||
&self,
|
||||
request_id: String,
|
||||
provider_id: String,
|
||||
app_type: String,
|
||||
model: String,
|
||||
usage: TokenUsage,
|
||||
cost_multiplier: Decimal,
|
||||
latency_ms: u64,
|
||||
status_code: u16,
|
||||
session_id: Option<String>,
|
||||
) -> Result<(), AppError> {
|
||||
let pricing = self.get_model_pricing(&model)?;
|
||||
|
||||
if pricing.is_none() {
|
||||
log::warn!("模型 {} 的定价信息未找到,成本将记录为 0", model);
|
||||
}
|
||||
|
||||
let cost = CostCalculator::try_calculate(&usage, pricing.as_ref(), cost_multiplier);
|
||||
|
||||
let log = RequestLog {
|
||||
request_id,
|
||||
provider_id,
|
||||
app_type,
|
||||
model,
|
||||
usage,
|
||||
cost,
|
||||
latency_ms,
|
||||
status_code,
|
||||
error_message: None,
|
||||
session_id,
|
||||
};
|
||||
|
||||
self.log_request(&log)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_log_request() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
// 插入测试定价
|
||||
{
|
||||
let conn = crate::database::lock_conn!(db.conn);
|
||||
conn.execute(
|
||||
"INSERT INTO model_pricing (model_id, display_name, input_cost_per_million, output_cost_per_million)
|
||||
VALUES ('test-model', 'Test Model', '3.0', '15.0')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let logger = UsageLogger::new(&db);
|
||||
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1000,
|
||||
output_tokens: 500,
|
||||
cache_read_tokens: 0,
|
||||
cache_creation_tokens: 0,
|
||||
};
|
||||
|
||||
logger
|
||||
.log_with_calculation(
|
||||
"req-123".to_string(),
|
||||
"provider-1".to_string(),
|
||||
"claude".to_string(),
|
||||
"test-model".to_string(),
|
||||
usage,
|
||||
Decimal::from(1),
|
||||
100,
|
||||
200,
|
||||
None,
|
||||
)?;
|
||||
|
||||
// 验证记录已插入
|
||||
let conn = crate::database::lock_conn!(db.conn);
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM proxy_request_logs WHERE request_id = 'req-123'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_error() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
let logger = UsageLogger::new(&db);
|
||||
|
||||
logger
|
||||
.log_error(
|
||||
"req-error".to_string(),
|
||||
"provider-1".to_string(),
|
||||
"claude".to_string(),
|
||||
"unknown-model".to_string(),
|
||||
500,
|
||||
"Internal Server Error".to_string(),
|
||||
50,
|
||||
)?;
|
||||
|
||||
// 验证错误记录已插入
|
||||
let conn = crate::database::lock_conn!(db.conn);
|
||||
let (status, error): (i64, Option<String>) = conn
|
||||
.query_row(
|
||||
"SELECT status_code, error_message FROM proxy_request_logs WHERE request_id = 'req-error'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(status, 500);
|
||||
assert_eq!(error, Some("Internal Server Error".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user