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:
Jason
2026-07-23 16:59:41 +08:00
parent 15d5dbe065
commit 3cf84ca362
4 changed files with 63 additions and 5 deletions
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+10 -1
View File
@@ -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;
+50 -2
View File
@@ -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.50input = (700-250)×2/1Mcache_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()?;