mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
fix(usage): centralize cache-inclusive app set and cover grokbuild in cost backfill
The cost backfill hardcoded codex|gemini as cache-inclusive apps, so grokbuild TOTAL-semantics rows were priced on full input tokens with cache reads double-counted. Converge the writer (proxy logger and calculator) and the backfill recompute onto a single sql_helpers::is_cache_inclusive_app predicate backed by the existing CACHE_INCLUSIVE_APP_TYPES constant, and add a regression test for the grokbuild backfill path.
This commit is contained in:
@@ -59,7 +59,8 @@ impl CostCalculator {
|
||||
pricing: &ModelPricing,
|
||||
cost_multiplier: Decimal,
|
||||
) -> CostBreakdown {
|
||||
let input_includes_cache_read = matches!(app_type, "codex" | "gemini" | "grokbuild");
|
||||
let input_includes_cache_read =
|
||||
crate::services::sql_helpers::is_cache_inclusive_app(app_type);
|
||||
Self::calculate_with_cache_semantics(
|
||||
usage,
|
||||
pricing,
|
||||
|
||||
@@ -122,7 +122,7 @@ impl<'a> UsageLogger<'a> {
|
||||
|
||||
let created_at = chrono::Utc::now().timestamp();
|
||||
let input_token_semantics =
|
||||
if matches!(log.app_type.as_str(), "codex" | "gemini" | "grokbuild") {
|
||||
if crate::services::sql_helpers::is_cache_inclusive_app(log.app_type.as_str()) {
|
||||
INPUT_TOKEN_SEMANTICS_TOTAL
|
||||
} else {
|
||||
INPUT_TOKEN_SEMANTICS_FRESH
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
/// style provider not added here) shows up loudly as a too-low cache hit
|
||||
/// rate, which is easier to catch than the silent over-deduction that
|
||||
/// would happen with the opposite default.
|
||||
const CACHE_INCLUSIVE_APP_TYPES: &[&str] = &["codex", "gemini", "grokbuild"];
|
||||
/// 单一语义集(SSOT):写入侧(proxy logger/calculator)、回填侧
|
||||
/// (usage_stats 成本重算)与展示侧(本文件的 SQL 归一)都必须引用这里,
|
||||
/// 防止同一语义散落多处后新增 app 时漏改(grokbuild 曾在回填侧漏掉)。
|
||||
/// 前端 `src/types/usage.ts` 的同名常量是跨语言的对应物,改动须同步。
|
||||
pub(crate) const CACHE_INCLUSIVE_APP_TYPES: &[&str] = &["codex", "gemini", "grokbuild"];
|
||||
|
||||
/// `app_type` 的存储 `input_tokens` 是否已包含 cache read/write。
|
||||
pub(crate) fn is_cache_inclusive_app(app_type: &str) -> bool {
|
||||
CACHE_INCLUSIVE_APP_TYPES.contains(&app_type)
|
||||
}
|
||||
|
||||
pub(crate) const INPUT_TOKEN_SEMANTICS_LEGACY: i64 = 0;
|
||||
pub(crate) const INPUT_TOKEN_SEMANTICS_TOTAL: i64 = 1;
|
||||
|
||||
@@ -1850,10 +1850,11 @@ impl Database {
|
||||
let million = rust_decimal::Decimal::from(1_000_000u64);
|
||||
|
||||
// 与 CostCalculator::calculate_for_app 保持一致的计算逻辑:
|
||||
// 1. 历史 Codex/Gemini 行只包含 cache read;新 total 行还包含 cache write。
|
||||
// 1. 历史 cache-inclusive 行只包含 cache read;新 total 行还包含 cache write。
|
||||
// 2. Claude/Anthropic 的 input_tokens 已经是 fresh input,不能再次扣减
|
||||
// 3. 各项成本是基础成本(不含倍率),倍率只作用于最终总价
|
||||
let cache_inclusive_app = matches!(log.app_type.as_str(), "codex" | "gemini");
|
||||
let cache_inclusive_app =
|
||||
crate::services::sql_helpers::is_cache_inclusive_app(log.app_type.as_str());
|
||||
let billable_input_tokens =
|
||||
if !cache_inclusive_app || log.input_token_semantics == INPUT_TOKEN_SEMANTICS_FRESH {
|
||||
log.input_tokens as u64
|
||||
@@ -2613,6 +2614,53 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backfill_deducts_cache_read_for_grokbuild_total_rows() -> Result<(), AppError> {
|
||||
// 回归:回填侧的 cache-inclusive 判定曾硬编码 codex|gemini 漏掉
|
||||
// grokbuild,导致 TOTAL 行按全量 input 计价、cache_read 双算。
|
||||
// 判定收敛到 sql_helpers::is_cache_inclusive_app 后按 450 fresh 计价。
|
||||
let db = Database::memory()?;
|
||||
{
|
||||
let conn = lock_conn!(db.conn);
|
||||
insert_usage_log(
|
||||
&conn,
|
||||
"grokbuild-total-backfill",
|
||||
"grokbuild",
|
||||
"_grok_session",
|
||||
"grok-4.5",
|
||||
"grok_session",
|
||||
1000,
|
||||
700,
|
||||
100,
|
||||
250,
|
||||
0,
|
||||
200,
|
||||
"0",
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE proxy_request_logs
|
||||
SET input_token_semantics = ?1
|
||||
WHERE request_id = 'grokbuild-total-backfill'",
|
||||
[INPUT_TOKEN_SEMANTICS_TOTAL],
|
||||
)?;
|
||||
}
|
||||
|
||||
assert_eq!(db.backfill_missing_usage_costs()?, 1);
|
||||
|
||||
let conn = lock_conn!(db.conn);
|
||||
let (input_cost, cache_read_cost, total_cost): (String, String, String) = conn.query_row(
|
||||
"SELECT input_cost_usd, cache_read_cost_usd, total_cost_usd
|
||||
FROM proxy_request_logs WHERE request_id = 'grokbuild-total-backfill'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|
||||
)?;
|
||||
// grok-4.5 定价 2/6/0.50:input = (700-250)×2/1M,cache_read = 250×0.5/1M
|
||||
assert_eq!(input_cost, "0.000900");
|
||||
assert_eq!(cache_read_cost, "0.000125");
|
||||
assert_eq!(total_cost, "0.001625");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backfill_missing_usage_costs_uses_stored_multiplier() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
Reference in New Issue
Block a user