mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -1,5 +1,7 @@
|
||||
use crate::observability::stats::{aggregate_usage_stats, parse_bounded_u32, round_to};
|
||||
use aether_billing::normalize_input_tokens_for_billing;
|
||||
use aether_billing::{
|
||||
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
|
||||
};
|
||||
use aether_data::repository::users::StoredUserSummary;
|
||||
use aether_data_contracts::repository::{
|
||||
provider_catalog::{StoredProviderCatalogEndpoint, StoredProviderCatalogProvider},
|
||||
@@ -318,9 +320,20 @@ pub fn admin_usage_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64
|
||||
}
|
||||
|
||||
pub fn admin_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||
item.input_tokens
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item))
|
||||
.saturating_add(item.cache_read_input_tokens)
|
||||
let api_format = item
|
||||
.endpoint_api_format
|
||||
.as_deref()
|
||||
.or(item.api_format.as_deref());
|
||||
let input_tokens = i64::try_from(item.input_tokens).unwrap_or(i64::MAX);
|
||||
let cache_creation_tokens =
|
||||
i64::try_from(admin_usage_cache_creation_tokens(item)).unwrap_or(i64::MAX);
|
||||
let cache_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
|
||||
normalize_total_input_context_for_cache_hit_rate(
|
||||
api_format,
|
||||
input_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
) as u64
|
||||
}
|
||||
|
||||
pub fn admin_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||
@@ -358,13 +371,15 @@ pub fn admin_usage_aggregation_by_model_json(
|
||||
limit: usize,
|
||||
) -> Value {
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64)> =
|
||||
BTreeMap::new();
|
||||
let mut grouped: BTreeMap<
|
||||
String,
|
||||
(u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64),
|
||||
> = BTreeMap::new();
|
||||
for item in usage {
|
||||
let key = item.model.clone();
|
||||
let entry = grouped
|
||||
.entry(key)
|
||||
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0));
|
||||
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0));
|
||||
entry.0 = entry.0.saturating_add(1);
|
||||
entry.1 = entry.1.saturating_add(item.total_tokens);
|
||||
entry.2 = entry.2.saturating_add(item.input_tokens);
|
||||
@@ -374,16 +389,19 @@ pub fn admin_usage_aggregation_by_model_json(
|
||||
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||
entry.5 = entry
|
||||
.5
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
.saturating_add(admin_usage_total_input_context(item));
|
||||
entry.6 = entry
|
||||
.6
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
entry.7 = entry
|
||||
.7
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
entry.8 = entry
|
||||
.8
|
||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
|
||||
entry.9 += item.total_cost_usd;
|
||||
entry.10 += item.actual_total_cost_usd;
|
||||
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
||||
entry.10 += item.total_cost_usd;
|
||||
entry.11 += item.actual_total_cost_usd;
|
||||
}
|
||||
|
||||
let mut items: Vec<Value> = grouped
|
||||
@@ -394,9 +412,10 @@ pub fn admin_usage_aggregation_by_model_json(
|
||||
(
|
||||
request_count,
|
||||
total_tokens,
|
||||
input_tokens,
|
||||
_input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens,
|
||||
total_input_context,
|
||||
cache_creation_tokens,
|
||||
cache_creation_ephemeral_5m_tokens,
|
||||
cache_creation_ephemeral_1h_tokens,
|
||||
@@ -410,9 +429,7 @@ pub fn admin_usage_aggregation_by_model_json(
|
||||
"request_count": request_count,
|
||||
"total_tokens": total_tokens,
|
||||
"effective_input_tokens": effective_input_tokens,
|
||||
"total_input_context": input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
"total_input_context": total_input_context,
|
||||
"output_tokens": output_tokens,
|
||||
"total_cost": round_to(total_cost, 6),
|
||||
"actual_cost": round_to(actual_cost, 6),
|
||||
@@ -421,9 +438,7 @@ pub fn admin_usage_aggregation_by_model_json(
|
||||
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||
"cache_read_tokens": cache_read_tokens,
|
||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||
input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
total_input_context,
|
||||
cache_read_tokens,
|
||||
),
|
||||
})
|
||||
@@ -464,6 +479,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
f64,
|
||||
f64,
|
||||
u64,
|
||||
@@ -488,6 +504,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
0.0,
|
||||
0,
|
||||
@@ -505,21 +522,24 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||
entry.6 = entry
|
||||
.6
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
.saturating_add(admin_usage_total_input_context(item));
|
||||
entry.7 = entry
|
||||
.7
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
entry.8 = entry
|
||||
.8
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
entry.9 = entry
|
||||
.9
|
||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
||||
entry.10 += item.total_cost_usd;
|
||||
entry.11 += item.actual_total_cost_usd;
|
||||
entry.12 = entry
|
||||
.12
|
||||
.saturating_add(item.response_time_ms.unwrap_or_default());
|
||||
entry.10 = entry.10.saturating_add(item.cache_read_input_tokens);
|
||||
entry.11 += item.total_cost_usd;
|
||||
entry.12 += item.actual_total_cost_usd;
|
||||
entry.13 = entry
|
||||
.13
|
||||
.saturating_add(item.response_time_ms.unwrap_or_default());
|
||||
entry.14 = entry
|
||||
.14
|
||||
.saturating_add(if admin_usage_is_success(item) { 1 } else { 0 });
|
||||
}
|
||||
|
||||
@@ -532,9 +552,10 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
provider_name,
|
||||
request_count,
|
||||
total_tokens,
|
||||
input_tokens,
|
||||
_input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens,
|
||||
total_input_context,
|
||||
cache_creation_tokens,
|
||||
cache_creation_ephemeral_5m_tokens,
|
||||
cache_creation_ephemeral_1h_tokens,
|
||||
@@ -562,9 +583,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
"request_count": request_count,
|
||||
"total_tokens": total_tokens,
|
||||
"effective_input_tokens": effective_input_tokens,
|
||||
"total_input_context": input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
"total_input_context": total_input_context,
|
||||
"output_tokens": output_tokens,
|
||||
"total_cost": round_to(total_cost, 6),
|
||||
"actual_cost": round_to(actual_cost, 6),
|
||||
@@ -576,9 +595,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
||||
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||
"cache_read_tokens": cache_read_tokens,
|
||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||
input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
total_input_context,
|
||||
cache_read_tokens,
|
||||
),
|
||||
})
|
||||
@@ -608,7 +625,21 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut grouped: BTreeMap<
|
||||
String,
|
||||
(u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64, u64),
|
||||
(
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
f64,
|
||||
f64,
|
||||
u64,
|
||||
),
|
||||
> = BTreeMap::new();
|
||||
for item in usage {
|
||||
let key = item
|
||||
@@ -617,7 +648,7 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let entry = grouped
|
||||
.entry(key)
|
||||
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0));
|
||||
.or_insert((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0));
|
||||
entry.0 = entry.0.saturating_add(1);
|
||||
entry.1 = entry.1.saturating_add(item.total_tokens);
|
||||
entry.2 = entry.2.saturating_add(item.input_tokens);
|
||||
@@ -627,18 +658,21 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||
entry.5 = entry
|
||||
.5
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
.saturating_add(admin_usage_total_input_context(item));
|
||||
entry.6 = entry
|
||||
.6
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||
entry.7 = entry
|
||||
.7
|
||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||
entry.8 = entry
|
||||
.8
|
||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
|
||||
entry.9 += item.total_cost_usd;
|
||||
entry.10 += item.actual_total_cost_usd;
|
||||
entry.11 = entry
|
||||
.11
|
||||
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
||||
entry.10 += item.total_cost_usd;
|
||||
entry.11 += item.actual_total_cost_usd;
|
||||
entry.12 = entry
|
||||
.12
|
||||
.saturating_add(item.response_time_ms.unwrap_or_default());
|
||||
}
|
||||
|
||||
@@ -650,9 +684,10 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
(
|
||||
request_count,
|
||||
total_tokens,
|
||||
input_tokens,
|
||||
_input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens,
|
||||
total_input_context,
|
||||
cache_creation_tokens,
|
||||
cache_creation_ephemeral_5m_tokens,
|
||||
cache_creation_ephemeral_1h_tokens,
|
||||
@@ -672,9 +707,7 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
"request_count": request_count,
|
||||
"total_tokens": total_tokens,
|
||||
"effective_input_tokens": effective_input_tokens,
|
||||
"total_input_context": input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
"total_input_context": total_input_context,
|
||||
"output_tokens": output_tokens,
|
||||
"total_cost": round_to(total_cost, 6),
|
||||
"actual_cost": round_to(actual_cost, 6),
|
||||
@@ -684,9 +717,7 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
||||
"cache_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||
"cache_read_tokens": cache_read_tokens,
|
||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||
input_tokens
|
||||
.saturating_add(cache_creation_tokens)
|
||||
.saturating_add(cache_read_tokens),
|
||||
total_input_context,
|
||||
cache_read_tokens,
|
||||
),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user