mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
fix(usage): pricing routing, SSE lifecycle, and validation hardening
* model pricing routing: extend prefix-match families (gpt-/o1-o5/
gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
thresholds so short base IDs like gpt-5 no longer mis-match
gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
byte matching so claude-haiku-4-5-20251001 falls back to
claude-haiku-4-5 pricing
* SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
early return or panic; AtomicBool fast path lets push() skip the
Mutex once first-event time is recorded
* validation: shared validate_cost_multiplier / validate_pricing_source
helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
PRICING_SOURCE_REQUEST constants replace string literals; price
fields in update_model_pricing now reject empty / non-decimal /
negative input before INSERT
* backfill: add backfill_missing_usage_costs_for_model so a single
price edit only scans matching rows instead of the full log table;
startup backfill remains full-scan
* session_usage{,_codex,_gemini}: share find_model_pricing helper from
usage_stats; metadata_modified_nanos centralizes mtime precision
* frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
replace three copies of the same multiplier regex; isUnpricedUsage
surfaces zero-cost rows that have usage tokens (cached per row to
avoid double evaluation); invalidate usageKeys.all on pricing mutate
so backfilled rows refresh
This commit is contained in:
@@ -2,12 +2,56 @@
|
||||
//!
|
||||
//! 处理代理配置、Provider健康状态和使用统计的数据库操作
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::proxy::types::*;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use super::super::{lock_conn, Database};
|
||||
|
||||
pub(crate) const PRICING_SOURCE_RESPONSE: &str = "response";
|
||||
pub(crate) const PRICING_SOURCE_REQUEST: &str = "request";
|
||||
|
||||
pub(crate) fn validate_cost_multiplier(value: &str) -> Result<Decimal, AppError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::localized(
|
||||
"error.multiplierEmpty",
|
||||
"倍率不能为空",
|
||||
"Multiplier cannot be empty",
|
||||
));
|
||||
}
|
||||
let parsed = Decimal::from_str(trimmed).map_err(|e| {
|
||||
AppError::localized(
|
||||
"error.invalidMultiplier",
|
||||
format!("无效倍率: {value} - {e}"),
|
||||
format!("Invalid multiplier: {value} - {e}"),
|
||||
)
|
||||
})?;
|
||||
if parsed < Decimal::ZERO {
|
||||
return Err(AppError::localized(
|
||||
"error.invalidMultiplier",
|
||||
format!("无效倍率: {value} - 倍率不能为负数"),
|
||||
format!("Invalid multiplier: {value} - multiplier cannot be negative"),
|
||||
));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_pricing_source(value: &str) -> Result<&str, AppError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed == PRICING_SOURCE_RESPONSE || trimmed == PRICING_SOURCE_REQUEST {
|
||||
Ok(trimmed)
|
||||
} else {
|
||||
Err(AppError::localized(
|
||||
"error.invalidPricingMode",
|
||||
format!("无效计费模式: {value}"),
|
||||
format!("Invalid pricing mode: {value}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
// ==================== Global Proxy Config ====================
|
||||
|
||||
@@ -103,21 +147,8 @@ impl Database {
|
||||
app_type: &str,
|
||||
value: &str,
|
||||
) -> Result<(), AppError> {
|
||||
validate_cost_multiplier(value)?;
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::localized(
|
||||
"error.multiplierEmpty",
|
||||
"倍率不能为空",
|
||||
"Multiplier cannot be empty",
|
||||
));
|
||||
}
|
||||
trimmed.parse::<Decimal>().map_err(|e| {
|
||||
AppError::localized(
|
||||
"error.invalidMultiplier",
|
||||
format!("无效倍率: {value} - {e}"),
|
||||
format!("Invalid multiplier: {value} - {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 确保行存在
|
||||
self.ensure_proxy_config_row_exists(app_type)?;
|
||||
@@ -150,7 +181,7 @@ impl Database {
|
||||
Ok(value) => Ok(value),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
self.init_proxy_config_rows().await?;
|
||||
Ok("response".to_string())
|
||||
Ok(PRICING_SOURCE_RESPONSE.to_string())
|
||||
}
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
@@ -162,14 +193,7 @@ impl Database {
|
||||
app_type: &str,
|
||||
value: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let trimmed = value.trim();
|
||||
if !matches!(trimmed, "response" | "request") {
|
||||
return Err(AppError::localized(
|
||||
"error.invalidPricingMode",
|
||||
format!("无效计费模式: {value}"),
|
||||
format!("Invalid pricing mode: {value}"),
|
||||
));
|
||||
}
|
||||
let trimmed = validate_pricing_source(value)?;
|
||||
|
||||
// 确保行存在
|
||||
self.ensure_proxy_config_row_exists(app_type)?;
|
||||
@@ -911,6 +935,18 @@ mod tests {
|
||||
}
|
||||
));
|
||||
|
||||
let err = db
|
||||
.set_default_cost_multiplier("claude", "-0.5")
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
AppError::Localized {
|
||||
key: "error.invalidMultiplier",
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ mod tests;
|
||||
|
||||
// DAO 类型导出供外部使用
|
||||
pub(crate) use dao::providers_seed::CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID;
|
||||
pub(crate) use dao::proxy::{
|
||||
validate_cost_multiplier, validate_pricing_source, PRICING_SOURCE_REQUEST,
|
||||
PRICING_SOURCE_RESPONSE,
|
||||
};
|
||||
pub use dao::FailoverQueueItem;
|
||||
|
||||
use crate::config::get_app_config_dir;
|
||||
|
||||
Reference in New Issue
Block a user