mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
8669b408e9
Extract message_id from Claude API responses (msg_xxx) and use it to
generate a shared request_id format (session:{msg_xxx}) between the
proxy logger and session log sync. When session sync encounters the
same request_id via INSERT OR IGNORE, it skips the duplicate.
- Add message_id field to TokenUsage, extracted from Claude responses
- Add TokenUsage::dedup_request_id() to generate shared request IDs
- Define SESSION_REQUEST_ID_PREFIX constant to eliminate magic strings
- Change proxy logger to INSERT OR REPLACE for richer-data-wins semantics
200 lines
6.5 KiB
Rust
200 lines
6.5 KiB
Rust
//! Cost Calculator - 计算 API 请求成本
|
||
//!
|
||
//! 使用高精度 Decimal 类型避免浮点数精度问题
|
||
|
||
use super::parser::TokenUsage;
|
||
use rust_decimal::Decimal;
|
||
use std::str::FromStr;
|
||
|
||
/// 成本明细
|
||
#[derive(Debug, Clone)]
|
||
pub struct CostBreakdown {
|
||
pub input_cost: Decimal,
|
||
pub output_cost: Decimal,
|
||
pub cache_read_cost: Decimal,
|
||
pub cache_creation_cost: Decimal,
|
||
pub total_cost: Decimal,
|
||
}
|
||
|
||
/// 模型定价信息
|
||
#[derive(Debug, Clone)]
|
||
pub struct ModelPricing {
|
||
pub input_cost_per_million: Decimal,
|
||
pub output_cost_per_million: Decimal,
|
||
pub cache_read_cost_per_million: Decimal,
|
||
pub cache_creation_cost_per_million: Decimal,
|
||
}
|
||
|
||
/// 成本计算器
|
||
pub struct CostCalculator;
|
||
|
||
impl CostCalculator {
|
||
/// 计算请求成本
|
||
///
|
||
/// # 参数
|
||
/// - `usage`: Token 使用量
|
||
/// - `pricing`: 模型定价
|
||
/// - `cost_multiplier`: 成本倍数 (provider 自定义)
|
||
///
|
||
/// # 计算逻辑
|
||
/// - input_cost: (input_tokens - cache_read_tokens) × 输入价格
|
||
/// - cache_read_cost: cache_read_tokens × 缓存读取价格
|
||
/// - 这样避免缓存部分被重复计费
|
||
/// - total_cost: 各项成本之和 × 倍率(倍率只作用于最终总价)
|
||
pub fn calculate(
|
||
usage: &TokenUsage,
|
||
pricing: &ModelPricing,
|
||
cost_multiplier: Decimal,
|
||
) -> CostBreakdown {
|
||
let million = Decimal::from(1_000_000);
|
||
|
||
// 计算实际需要按输入价格计费的 token 数(减去缓存命中部分)
|
||
let billable_input_tokens = usage.input_tokens.saturating_sub(usage.cache_read_tokens);
|
||
|
||
// 各项基础成本(不含倍率)
|
||
let input_cost =
|
||
Decimal::from(billable_input_tokens) * pricing.input_cost_per_million / million;
|
||
let output_cost =
|
||
Decimal::from(usage.output_tokens) * pricing.output_cost_per_million / million;
|
||
let cache_read_cost =
|
||
Decimal::from(usage.cache_read_tokens) * pricing.cache_read_cost_per_million / million;
|
||
let cache_creation_cost = Decimal::from(usage.cache_creation_tokens)
|
||
* pricing.cache_creation_cost_per_million
|
||
/ million;
|
||
|
||
// 总成本 = 各项基础成本之和 × 倍率
|
||
let base_total = input_cost + output_cost + cache_read_cost + cache_creation_cost;
|
||
let total_cost = base_total * cost_multiplier;
|
||
|
||
CostBreakdown {
|
||
input_cost,
|
||
output_cost,
|
||
cache_read_cost,
|
||
cache_creation_cost,
|
||
total_cost,
|
||
}
|
||
}
|
||
|
||
/// 尝试计算成本,如果模型未知则返回 None
|
||
pub fn try_calculate(
|
||
usage: &TokenUsage,
|
||
pricing: Option<&ModelPricing>,
|
||
cost_multiplier: Decimal,
|
||
) -> Option<CostBreakdown> {
|
||
pricing.map(|p| Self::calculate(usage, p, cost_multiplier))
|
||
}
|
||
}
|
||
|
||
impl ModelPricing {
|
||
/// 从字符串创建定价信息
|
||
pub fn from_strings(
|
||
input: &str,
|
||
output: &str,
|
||
cache_read: &str,
|
||
cache_creation: &str,
|
||
) -> Result<Self, rust_decimal::Error> {
|
||
Ok(Self {
|
||
input_cost_per_million: Decimal::from_str(input)?,
|
||
output_cost_per_million: Decimal::from_str(output)?,
|
||
cache_read_cost_per_million: Decimal::from_str(cache_read)?,
|
||
cache_creation_cost_per_million: Decimal::from_str(cache_creation)?,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_cost_calculation() {
|
||
let usage = TokenUsage {
|
||
input_tokens: 1000,
|
||
output_tokens: 500,
|
||
cache_read_tokens: 200,
|
||
cache_creation_tokens: 100,
|
||
model: None,
|
||
message_id: None,
|
||
};
|
||
|
||
let pricing = ModelPricing::from_strings("3.0", "15.0", "0.3", "3.75").unwrap();
|
||
let multiplier = Decimal::from_str("1.0").unwrap();
|
||
|
||
let cost = CostCalculator::calculate(&usage, &pricing, multiplier);
|
||
|
||
// input: (1000 - 200) * 3.0 / 1M = 0.0024 (只计算非缓存部分)
|
||
assert_eq!(cost.input_cost, Decimal::from_str("0.0024").unwrap());
|
||
// output: 500 * 15.0 / 1M = 0.0075
|
||
assert_eq!(cost.output_cost, Decimal::from_str("0.0075").unwrap());
|
||
// cache_read: 200 * 0.3 / 1M = 0.00006
|
||
assert_eq!(cost.cache_read_cost, Decimal::from_str("0.00006").unwrap());
|
||
// cache_creation: 100 * 3.75 / 1M = 0.000375
|
||
assert_eq!(
|
||
cost.cache_creation_cost,
|
||
Decimal::from_str("0.000375").unwrap()
|
||
);
|
||
// total: 0.0024 + 0.0075 + 0.00006 + 0.000375 = 0.010335
|
||
assert_eq!(cost.total_cost, Decimal::from_str("0.010335").unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn test_cost_multiplier() {
|
||
let usage = TokenUsage {
|
||
input_tokens: 1000,
|
||
output_tokens: 0,
|
||
cache_read_tokens: 0,
|
||
cache_creation_tokens: 0,
|
||
model: None,
|
||
message_id: None,
|
||
};
|
||
|
||
let pricing = ModelPricing::from_strings("3.0", "15.0", "0", "0").unwrap();
|
||
let multiplier = Decimal::from_str("1.5").unwrap();
|
||
|
||
let cost = CostCalculator::calculate(&usage, &pricing, multiplier);
|
||
|
||
// input_cost: 基础价格(不含倍率)= 1000 * 3.0 / 1M = 0.003
|
||
assert_eq!(cost.input_cost, Decimal::from_str("0.003").unwrap());
|
||
// total_cost: 基础价格 × 倍率 = 0.003 * 1.5 = 0.0045
|
||
assert_eq!(cost.total_cost, Decimal::from_str("0.0045").unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn test_unknown_model_handling() {
|
||
let usage = TokenUsage {
|
||
input_tokens: 1000,
|
||
output_tokens: 500,
|
||
cache_read_tokens: 0,
|
||
cache_creation_tokens: 0,
|
||
model: None,
|
||
message_id: None,
|
||
};
|
||
|
||
let multiplier = Decimal::from_str("1.0").unwrap();
|
||
let cost = CostCalculator::try_calculate(&usage, None, multiplier);
|
||
|
||
assert!(cost.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_decimal_precision() {
|
||
let usage = TokenUsage {
|
||
input_tokens: 1,
|
||
output_tokens: 1,
|
||
cache_read_tokens: 1,
|
||
cache_creation_tokens: 1,
|
||
model: None,
|
||
message_id: None,
|
||
};
|
||
|
||
let pricing = ModelPricing::from_strings("0.075", "0.3", "0.01875", "0.075").unwrap();
|
||
let multiplier = Decimal::from_str("1.0").unwrap();
|
||
|
||
let cost = CostCalculator::calculate(&usage, &pricing, multiplier);
|
||
|
||
// 验证高精度计算
|
||
assert!(cost.total_cost > Decimal::ZERO);
|
||
assert!(cost.total_cost.to_string().len() > 2); // 确保保留了小数位
|
||
}
|
||
}
|