fix(usage): 统一使用记录与仪表盘的缓存命中率计算口径

- 新增归一化总输入上下文计算逻辑,按 provider 区分 OpenAI/Gemini 与 Claude 的 cache token 语义
- 将管理端使用聚合、用户使用记录、仪表盘缓存统计、缓存亲和性分析统一为 token 级缓存命中率
- 修正 total_input_context 字段,避免 cache_read 在部分 provider 上被重复计入分母
- 同步更新相关 Rust 单元测试与网关集成测试断言
- 调整前端 dashboard mock 中 cache_hit_rate 的单位为百分比
This commit is contained in:
AAEE86
2026-04-12 00:57:40 +08:00
parent 7c5bb7f383
commit 335e440cc5
9 changed files with 218 additions and 74 deletions

View File

@@ -27,4 +27,6 @@ pub use schema::{
BillingSnapshot, BillingSnapshotStatus, CostResult, BILLING_SNAPSHOT_SCHEMA_VERSION,
};
pub use service::BillingService;
pub use token_normalization::normalize_input_tokens_for_billing;
pub use token_normalization::{
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
};

View File

@@ -43,9 +43,42 @@ pub fn normalize_input_tokens_for_billing(
}
}
pub fn normalize_total_input_context_for_cache_hit_rate(
api_format: Option<&str>,
input_tokens: i64,
cache_creation_tokens: i64,
cache_read_tokens: i64,
) -> i64 {
let normalized_input_tokens = input_tokens.max(0);
let normalized_cache_creation_tokens = cache_creation_tokens.max(0);
let normalized_cache_read_tokens = cache_read_tokens.max(0);
let fresh_input_tokens = match parse_api_family(api_format) {
ApiFamily::Claude => {
normalized_input_tokens.saturating_add(normalized_cache_creation_tokens)
}
ApiFamily::OpenAi | ApiFamily::Gemini => normalize_input_tokens_for_billing(
api_format,
normalized_input_tokens,
normalized_cache_read_tokens,
),
ApiFamily::Unknown => {
if normalized_cache_creation_tokens > 0 {
normalized_input_tokens.saturating_add(normalized_cache_creation_tokens)
} else {
normalized_input_tokens
}
}
};
fresh_input_tokens.saturating_add(normalized_cache_read_tokens)
}
#[cfg(test)]
mod tests {
use super::normalize_input_tokens_for_billing;
use super::{
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
};
#[test]
fn subtracts_cache_tokens_for_openai_and_gemini() {
@@ -66,4 +99,36 @@ mod tests {
100
);
}
#[test]
fn normalizes_cache_hit_context_for_openai_and_gemini() {
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(Some("openai:chat"), 120, 10, 15),
120
);
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(Some("gemini:chat"), 120, 10, 15),
120
);
}
#[test]
fn includes_cache_creation_for_claude_cache_hit_context() {
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(Some("claude:chat"), 60, 15, 5),
80
);
}
#[test]
fn falls_back_to_creation_aware_context_for_unknown_formats() {
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(None, 20, 10, 5),
35
);
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(None, 20, 0, 5),
25
);
}
}