mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 06:24:32 +08:00
785e1b5add
* feat(db): add pricing config fields to proxy_config table - Add default_cost_multiplier field per app type - Add pricing_model_source field (request/response) - Add request_model field to proxy_request_logs table - Implement schema migration v5 * feat(api): add pricing config commands and provider meta fields - Add get/set commands for default cost multiplier - Add get/set commands for pricing model source - Extend ProviderMeta with cost_multiplier and pricing_model_source - Register new commands in Tauri invoke handler * fix(proxy): apply cost multiplier to total cost only - Move multiplier calculation from per-item to total cost - Add resolve_pricing_config for provider-level override - Include request_model and cost_multiplier in usage logs - Return new fields in get_request_logs API * feat(ui): add pricing config UI and usage log enhancements - Add pricing config section to provider advanced settings - Refactor PricingConfigPanel to compact table layout - Display all three apps (Claude/Codex/Gemini) in one view - Add multiplier column and request model display to logs - Add frontend API wrappers for pricing config * feat(i18n): add pricing config translations - Add zh/en/ja translations for pricing defaults config - Add translations for multiplier, requestModel, responseModel - Add provider pricing config translations * fix(pricing): align backfill cost calculation with real-time logic - Fix backfill to deduct cache_read_tokens from input (avoid double billing) - Apply multiplier only to total cost, not to each item - Add multiplier display in request detail panel with i18n support - Use AppError::localized for backend error messages - Fix init_proxy_config_rows to use per-app default values - Fix silent failure in set_default_cost_multiplier/set_pricing_model_source - Add clippy allow annotation for test mutex across await * style: format code with cargo fmt and prettier * fix(tests): correct error type assertions in proxy DAO tests The tests expected AppError::InvalidInput but the DAO functions use AppError::localized() which returns AppError::Localized variant. Updated assertions to match the correct error type with key validation. --------- Co-authored-by: Jason <farion1231@gmail.com>
196 lines
6.4 KiB
Rust
196 lines
6.4 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,
|
||
};
|
||
|
||
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,
|
||
};
|
||
|
||
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,
|
||
};
|
||
|
||
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,
|
||
};
|
||
|
||
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); // 确保保留了小数位
|
||
}
|
||
}
|