mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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:
@@ -4,8 +4,9 @@ use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
|||||||
use crate::handlers::admin::shared::query_param_value;
|
use crate::handlers::admin::shared::query_param_value;
|
||||||
use crate::GatewayError;
|
use crate::GatewayError;
|
||||||
use aether_admin::observability::usage::{
|
use aether_admin::observability::usage::{
|
||||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
admin_usage_bad_request_response, admin_usage_cache_creation_tokens,
|
||||||
admin_usage_matches_optional_id, admin_usage_parse_recent_hours,
|
admin_usage_data_unavailable_response, admin_usage_matches_optional_id,
|
||||||
|
admin_usage_parse_recent_hours, admin_usage_total_input_context,
|
||||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
@@ -50,10 +51,9 @@ pub(super) async fn build_admin_usage_cache_affinity_hit_analysis_response(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|item| item.cache_read_input_tokens)
|
.map(|item| item.cache_read_input_tokens)
|
||||||
.sum();
|
.sum();
|
||||||
let total_cache_creation_tokens: u64 = filtered
|
let total_cache_creation_tokens: u64 =
|
||||||
.iter()
|
filtered.iter().map(admin_usage_cache_creation_tokens).sum();
|
||||||
.map(|item| item.cache_creation_input_tokens)
|
let total_input_context: u64 = filtered.iter().map(admin_usage_total_input_context).sum();
|
||||||
.sum();
|
|
||||||
let total_cache_read_cost: f64 = filtered.iter().map(|item| item.cache_read_cost_usd).sum();
|
let total_cache_read_cost: f64 = filtered.iter().map(|item| item.cache_read_cost_usd).sum();
|
||||||
let total_cache_creation_cost: f64 = filtered
|
let total_cache_creation_cost: f64 = filtered
|
||||||
.iter()
|
.iter()
|
||||||
@@ -63,12 +63,11 @@ pub(super) async fn build_admin_usage_cache_affinity_hit_analysis_response(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|item| item.cache_read_input_tokens > 0)
|
.filter(|item| item.cache_read_input_tokens > 0)
|
||||||
.count();
|
.count();
|
||||||
let total_context_tokens = total_input_tokens.saturating_add(total_cache_read_tokens);
|
let token_cache_hit_rate = if total_input_context == 0 {
|
||||||
let token_cache_hit_rate = if total_context_tokens == 0 {
|
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
round_to(
|
round_to(
|
||||||
total_cache_read_tokens as f64 / total_context_tokens as f64 * 100.0,
|
total_cache_read_tokens as f64 / total_input_context as f64 * 100.0,
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::{
|
|||||||
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
|
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
|
||||||
GatewayError, GatewayPublicRequestContext,
|
GatewayError, GatewayPublicRequestContext,
|
||||||
};
|
};
|
||||||
|
use aether_billing::normalize_total_input_context_for_cache_hit_rate;
|
||||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
@@ -28,6 +29,7 @@ struct DashboardUsageTotals {
|
|||||||
total_tokens: u64,
|
total_tokens: u64,
|
||||||
cache_creation_tokens: u64,
|
cache_creation_tokens: u64,
|
||||||
cache_read_tokens: u64,
|
cache_read_tokens: u64,
|
||||||
|
cache_hit_total_input_context: u64,
|
||||||
cache_creation_cost_usd: f64,
|
cache_creation_cost_usd: f64,
|
||||||
cache_read_cost_usd: f64,
|
cache_read_cost_usd: f64,
|
||||||
total_cost_usd: f64,
|
total_cost_usd: f64,
|
||||||
@@ -69,12 +71,14 @@ pub(super) fn decision_route_kind(request_context: &GatewayPublicRequestContext)
|
|||||||
|
|
||||||
impl DashboardUsageTotals {
|
impl DashboardUsageTotals {
|
||||||
fn record(&mut self, item: &StoredRequestUsageAudit) {
|
fn record(&mut self, item: &StoredRequestUsageAudit) {
|
||||||
|
let cache_creation_tokens = dashboard_cache_creation_tokens(item);
|
||||||
self.requests += 1;
|
self.requests += 1;
|
||||||
self.input_tokens += item.input_tokens;
|
self.input_tokens += item.input_tokens;
|
||||||
self.output_tokens += item.output_tokens;
|
self.output_tokens += item.output_tokens;
|
||||||
self.total_tokens += item.total_tokens;
|
self.total_tokens += item.total_tokens;
|
||||||
self.cache_creation_tokens += item.cache_creation_input_tokens;
|
self.cache_creation_tokens += cache_creation_tokens;
|
||||||
self.cache_read_tokens += item.cache_read_input_tokens;
|
self.cache_read_tokens += item.cache_read_input_tokens;
|
||||||
|
self.cache_hit_total_input_context += dashboard_total_input_context(item);
|
||||||
self.cache_creation_cost_usd += item.cache_creation_cost_usd;
|
self.cache_creation_cost_usd += item.cache_creation_cost_usd;
|
||||||
self.cache_read_cost_usd += item.cache_read_cost_usd;
|
self.cache_read_cost_usd += item.cache_read_cost_usd;
|
||||||
self.total_cost_usd += item.total_cost_usd;
|
self.total_cost_usd += item.total_cost_usd;
|
||||||
@@ -102,15 +106,45 @@ impl DashboardUsageTotals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cache_hit_rate(&self) -> f64 {
|
fn cache_hit_rate(&self) -> f64 {
|
||||||
let total_cache_tokens = self.cache_creation_tokens + self.cache_read_tokens;
|
if self.cache_hit_total_input_context == 0 {
|
||||||
if total_cache_tokens == 0 {
|
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
dashboard_round_f64(self.cache_read_tokens as f64 / total_cache_tokens as f64, 4)
|
dashboard_round_f64(
|
||||||
|
self.cache_read_tokens as f64 / self.cache_hit_total_input_context as f64 * 100.0,
|
||||||
|
2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dashboard_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
|
let classified = item
|
||||||
|
.cache_creation_ephemeral_5m_input_tokens
|
||||||
|
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||||
|
if item.cache_creation_input_tokens == 0 && classified > 0 {
|
||||||
|
classified
|
||||||
|
} else {
|
||||||
|
item.cache_creation_input_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dashboard_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
|
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(dashboard_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
|
||||||
|
}
|
||||||
|
|
||||||
fn dashboard_round_f64(value: f64, decimals: u32) -> f64 {
|
fn dashboard_round_f64(value: f64, decimals: u32) -> f64 {
|
||||||
let factor = 10_f64.powi(i32::try_from(decimals).unwrap_or_default());
|
let factor = 10_f64.powi(i32::try_from(decimals).unwrap_or_default());
|
||||||
(value * factor).round() / factor
|
(value * factor).round() / factor
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
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_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
@@ -101,9 +103,20 @@ fn users_me_usage_cache_creation_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn users_me_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
fn users_me_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
item.input_tokens
|
let api_format = item
|
||||||
.saturating_add(users_me_usage_cache_creation_tokens(item))
|
.endpoint_api_format
|
||||||
.saturating_add(item.cache_read_input_tokens)
|
.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(users_me_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
|
||||||
}
|
}
|
||||||
|
|
||||||
fn users_me_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
fn users_me_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
|
|||||||
@@ -393,11 +393,11 @@ async fn gateway_handles_admin_usage_aggregation_stats_locally_with_trusted_admi
|
|||||||
assert_eq!(items[0]["request_count"], 2);
|
assert_eq!(items[0]["request_count"], 2);
|
||||||
assert_eq!(items[0]["output_tokens"], 40);
|
assert_eq!(items[0]["output_tokens"], 40);
|
||||||
assert_eq!(items[0]["effective_input_tokens"], 150);
|
assert_eq!(items[0]["effective_input_tokens"], 150);
|
||||||
assert_eq!(items[0]["total_input_context"], 200);
|
assert_eq!(items[0]["total_input_context"], 160);
|
||||||
assert_eq!(items[0]["cache_creation_tokens"], 30);
|
assert_eq!(items[0]["cache_creation_tokens"], 30);
|
||||||
assert_eq!(items[0]["cache_creation_ephemeral_5m_tokens"], 12);
|
assert_eq!(items[0]["cache_creation_ephemeral_5m_tokens"], 12);
|
||||||
assert_eq!(items[0]["cache_creation_ephemeral_1h_tokens"], 18);
|
assert_eq!(items[0]["cache_creation_ephemeral_1h_tokens"], 18);
|
||||||
assert_eq!(items[0]["cache_hit_rate"], 5.0);
|
assert_eq!(items[0]["cache_hit_rate"], 6.25);
|
||||||
assert_eq!(items[1]["model"], "claude-3-7");
|
assert_eq!(items[1]["model"], "claude-3-7");
|
||||||
assert_eq!(items[1]["output_tokens"], 20);
|
assert_eq!(items[1]["output_tokens"], 20);
|
||||||
|
|
||||||
@@ -1498,7 +1498,7 @@ async fn gateway_handles_admin_usage_cache_affinity_hit_analysis_locally_with_tr
|
|||||||
assert_eq!(payload["total_input_tokens"], 140);
|
assert_eq!(payload["total_input_tokens"], 140);
|
||||||
assert_eq!(payload["total_cache_read_tokens"], 50);
|
assert_eq!(payload["total_cache_read_tokens"], 50);
|
||||||
assert_eq!(payload["total_cache_creation_tokens"], 15);
|
assert_eq!(payload["total_cache_creation_tokens"], 15);
|
||||||
assert_eq!(payload["token_cache_hit_rate"], 26.32);
|
assert_eq!(payload["token_cache_hit_rate"], 35.71);
|
||||||
assert_eq!(payload["total_cache_read_cost_usd"], 0.02);
|
assert_eq!(payload["total_cache_read_cost_usd"], 0.02);
|
||||||
assert_eq!(payload["total_cache_creation_cost_usd"], 0.015);
|
assert_eq!(payload["total_cache_creation_cost_usd"], 0.015);
|
||||||
assert_eq!(payload["estimated_savings_usd"], 0.18);
|
assert_eq!(payload["estimated_savings_usd"], 0.18);
|
||||||
|
|||||||
@@ -4763,7 +4763,7 @@ async fn gateway_handles_users_me_usage_locally_without_proxying_upstream() {
|
|||||||
payload["summary_by_model"][0]["effective_input_tokens"],
|
payload["summary_by_model"][0]["effective_input_tokens"],
|
||||||
105
|
105
|
||||||
);
|
);
|
||||||
assert_eq!(payload["summary_by_model"][0]["total_input_context"], 145);
|
assert_eq!(payload["summary_by_model"][0]["total_input_context"], 120);
|
||||||
assert_eq!(payload["billing"]["id"], "wallet-auth-1");
|
assert_eq!(payload["billing"]["id"], "wallet-auth-1");
|
||||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::observability::stats::{aggregate_usage_stats, parse_bounded_u32, round_to};
|
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::repository::users::StoredUserSummary;
|
||||||
use aether_data_contracts::repository::{
|
use aether_data_contracts::repository::{
|
||||||
provider_catalog::{StoredProviderCatalogEndpoint, StoredProviderCatalogProvider},
|
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 {
|
pub fn admin_usage_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
item.input_tokens
|
let api_format = item
|
||||||
.saturating_add(admin_usage_cache_creation_tokens(item))
|
.endpoint_api_format
|
||||||
.saturating_add(item.cache_read_input_tokens)
|
.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 {
|
pub fn admin_usage_effective_input_tokens(item: &StoredRequestUsageAudit) -> u64 {
|
||||||
@@ -358,13 +371,15 @@ pub fn admin_usage_aggregation_by_model_json(
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
let mut grouped: BTreeMap<String, (u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64)> =
|
let mut grouped: BTreeMap<
|
||||||
BTreeMap::new();
|
String,
|
||||||
|
(u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, f64, f64),
|
||||||
|
> = BTreeMap::new();
|
||||||
for item in usage {
|
for item in usage {
|
||||||
let key = item.model.clone();
|
let key = item.model.clone();
|
||||||
let entry = grouped
|
let entry = grouped
|
||||||
.entry(key)
|
.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.0 = entry.0.saturating_add(1);
|
||||||
entry.1 = entry.1.saturating_add(item.total_tokens);
|
entry.1 = entry.1.saturating_add(item.total_tokens);
|
||||||
entry.2 = entry.2.saturating_add(item.input_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));
|
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||||
entry.5 = entry
|
entry.5 = entry
|
||||||
.5
|
.5
|
||||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
.saturating_add(admin_usage_total_input_context(item));
|
||||||
entry.6 = entry
|
entry.6 = entry
|
||||||
.6
|
.6
|
||||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||||
entry.7 = entry
|
entry.7 = entry
|
||||||
.7
|
.7
|
||||||
|
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||||
|
entry.8 = entry
|
||||||
|
.8
|
||||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||||
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
|
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
||||||
entry.9 += item.total_cost_usd;
|
entry.10 += item.total_cost_usd;
|
||||||
entry.10 += item.actual_total_cost_usd;
|
entry.11 += item.actual_total_cost_usd;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut items: Vec<Value> = grouped
|
let mut items: Vec<Value> = grouped
|
||||||
@@ -394,9 +412,10 @@ pub fn admin_usage_aggregation_by_model_json(
|
|||||||
(
|
(
|
||||||
request_count,
|
request_count,
|
||||||
total_tokens,
|
total_tokens,
|
||||||
input_tokens,
|
_input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
effective_input_tokens,
|
effective_input_tokens,
|
||||||
|
total_input_context,
|
||||||
cache_creation_tokens,
|
cache_creation_tokens,
|
||||||
cache_creation_ephemeral_5m_tokens,
|
cache_creation_ephemeral_5m_tokens,
|
||||||
cache_creation_ephemeral_1h_tokens,
|
cache_creation_ephemeral_1h_tokens,
|
||||||
@@ -410,9 +429,7 @@ pub fn admin_usage_aggregation_by_model_json(
|
|||||||
"request_count": request_count,
|
"request_count": request_count,
|
||||||
"total_tokens": total_tokens,
|
"total_tokens": total_tokens,
|
||||||
"effective_input_tokens": effective_input_tokens,
|
"effective_input_tokens": effective_input_tokens,
|
||||||
"total_input_context": input_tokens
|
"total_input_context": total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
"output_tokens": output_tokens,
|
"output_tokens": output_tokens,
|
||||||
"total_cost": round_to(total_cost, 6),
|
"total_cost": round_to(total_cost, 6),
|
||||||
"actual_cost": round_to(actual_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_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||||
"cache_read_tokens": cache_read_tokens,
|
"cache_read_tokens": cache_read_tokens,
|
||||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||||
input_tokens
|
total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
cache_read_tokens,
|
cache_read_tokens,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -464,6 +479,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
|||||||
u64,
|
u64,
|
||||||
u64,
|
u64,
|
||||||
u64,
|
u64,
|
||||||
|
u64,
|
||||||
f64,
|
f64,
|
||||||
f64,
|
f64,
|
||||||
u64,
|
u64,
|
||||||
@@ -488,6 +504,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
0,
|
||||||
0.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));
|
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||||
entry.6 = entry
|
entry.6 = entry
|
||||||
.6
|
.6
|
||||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
.saturating_add(admin_usage_total_input_context(item));
|
||||||
entry.7 = entry
|
entry.7 = entry
|
||||||
.7
|
.7
|
||||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||||
entry.8 = entry
|
entry.8 = entry
|
||||||
.8
|
.8
|
||||||
|
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||||
|
entry.9 = entry
|
||||||
|
.9
|
||||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||||
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
entry.10 = entry.10.saturating_add(item.cache_read_input_tokens);
|
||||||
entry.10 += item.total_cost_usd;
|
entry.11 += item.total_cost_usd;
|
||||||
entry.11 += item.actual_total_cost_usd;
|
entry.12 += item.actual_total_cost_usd;
|
||||||
entry.12 = entry
|
|
||||||
.12
|
|
||||||
.saturating_add(item.response_time_ms.unwrap_or_default());
|
|
||||||
entry.13 = entry
|
entry.13 = entry
|
||||||
.13
|
.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 });
|
.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,
|
provider_name,
|
||||||
request_count,
|
request_count,
|
||||||
total_tokens,
|
total_tokens,
|
||||||
input_tokens,
|
_input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
effective_input_tokens,
|
effective_input_tokens,
|
||||||
|
total_input_context,
|
||||||
cache_creation_tokens,
|
cache_creation_tokens,
|
||||||
cache_creation_ephemeral_5m_tokens,
|
cache_creation_ephemeral_5m_tokens,
|
||||||
cache_creation_ephemeral_1h_tokens,
|
cache_creation_ephemeral_1h_tokens,
|
||||||
@@ -562,9 +583,7 @@ pub fn admin_usage_aggregation_by_provider_json(
|
|||||||
"request_count": request_count,
|
"request_count": request_count,
|
||||||
"total_tokens": total_tokens,
|
"total_tokens": total_tokens,
|
||||||
"effective_input_tokens": effective_input_tokens,
|
"effective_input_tokens": effective_input_tokens,
|
||||||
"total_input_context": input_tokens
|
"total_input_context": total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
"output_tokens": output_tokens,
|
"output_tokens": output_tokens,
|
||||||
"total_cost": round_to(total_cost, 6),
|
"total_cost": round_to(total_cost, 6),
|
||||||
"actual_cost": round_to(actual_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_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||||
"cache_read_tokens": cache_read_tokens,
|
"cache_read_tokens": cache_read_tokens,
|
||||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||||
input_tokens
|
total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
cache_read_tokens,
|
cache_read_tokens,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -608,7 +625,21 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
let mut grouped: BTreeMap<
|
let mut grouped: BTreeMap<
|
||||||
String,
|
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();
|
> = BTreeMap::new();
|
||||||
for item in usage {
|
for item in usage {
|
||||||
let key = item
|
let key = item
|
||||||
@@ -617,7 +648,7 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
|||||||
.unwrap_or_else(|| "unknown".to_string());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
let entry = grouped
|
let entry = grouped
|
||||||
.entry(key)
|
.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.0 = entry.0.saturating_add(1);
|
||||||
entry.1 = entry.1.saturating_add(item.total_tokens);
|
entry.1 = entry.1.saturating_add(item.total_tokens);
|
||||||
entry.2 = entry.2.saturating_add(item.input_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));
|
.saturating_add(admin_usage_effective_input_tokens(item));
|
||||||
entry.5 = entry
|
entry.5 = entry
|
||||||
.5
|
.5
|
||||||
.saturating_add(admin_usage_cache_creation_tokens(item));
|
.saturating_add(admin_usage_total_input_context(item));
|
||||||
entry.6 = entry
|
entry.6 = entry
|
||||||
.6
|
.6
|
||||||
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
.saturating_add(admin_usage_cache_creation_tokens(item));
|
||||||
entry.7 = entry
|
entry.7 = entry
|
||||||
.7
|
.7
|
||||||
|
.saturating_add(item.cache_creation_ephemeral_5m_input_tokens);
|
||||||
|
entry.8 = entry
|
||||||
|
.8
|
||||||
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
.saturating_add(item.cache_creation_ephemeral_1h_input_tokens);
|
||||||
entry.8 = entry.8.saturating_add(item.cache_read_input_tokens);
|
entry.9 = entry.9.saturating_add(item.cache_read_input_tokens);
|
||||||
entry.9 += item.total_cost_usd;
|
entry.10 += item.total_cost_usd;
|
||||||
entry.10 += item.actual_total_cost_usd;
|
entry.11 += item.actual_total_cost_usd;
|
||||||
entry.11 = entry
|
entry.12 = entry
|
||||||
.11
|
.12
|
||||||
.saturating_add(item.response_time_ms.unwrap_or_default());
|
.saturating_add(item.response_time_ms.unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,9 +684,10 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
|||||||
(
|
(
|
||||||
request_count,
|
request_count,
|
||||||
total_tokens,
|
total_tokens,
|
||||||
input_tokens,
|
_input_tokens,
|
||||||
output_tokens,
|
output_tokens,
|
||||||
effective_input_tokens,
|
effective_input_tokens,
|
||||||
|
total_input_context,
|
||||||
cache_creation_tokens,
|
cache_creation_tokens,
|
||||||
cache_creation_ephemeral_5m_tokens,
|
cache_creation_ephemeral_5m_tokens,
|
||||||
cache_creation_ephemeral_1h_tokens,
|
cache_creation_ephemeral_1h_tokens,
|
||||||
@@ -672,9 +707,7 @@ pub fn admin_usage_aggregation_by_api_format_json(
|
|||||||
"request_count": request_count,
|
"request_count": request_count,
|
||||||
"total_tokens": total_tokens,
|
"total_tokens": total_tokens,
|
||||||
"effective_input_tokens": effective_input_tokens,
|
"effective_input_tokens": effective_input_tokens,
|
||||||
"total_input_context": input_tokens
|
"total_input_context": total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
"output_tokens": output_tokens,
|
"output_tokens": output_tokens,
|
||||||
"total_cost": round_to(total_cost, 6),
|
"total_cost": round_to(total_cost, 6),
|
||||||
"actual_cost": round_to(actual_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_creation_ephemeral_1h_tokens": cache_creation_ephemeral_1h_tokens,
|
||||||
"cache_read_tokens": cache_read_tokens,
|
"cache_read_tokens": cache_read_tokens,
|
||||||
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
"cache_hit_rate": admin_usage_token_cache_hit_rate(
|
||||||
input_tokens
|
total_input_context,
|
||||||
.saturating_add(cache_creation_tokens)
|
|
||||||
.saturating_add(cache_read_tokens),
|
|
||||||
cache_read_tokens,
|
cache_read_tokens,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,4 +27,6 @@ pub use schema::{
|
|||||||
BillingSnapshot, BillingSnapshotStatus, CostResult, BILLING_SNAPSHOT_SCHEMA_VERSION,
|
BillingSnapshot, BillingSnapshotStatus, CostResult, BILLING_SNAPSHOT_SCHEMA_VERSION,
|
||||||
};
|
};
|
||||||
pub use service::BillingService;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
#[test]
|
||||||
fn subtracts_cache_tokens_for_openai_and_gemini() {
|
fn subtracts_cache_tokens_for_openai_and_gemini() {
|
||||||
@@ -66,4 +99,36 @@ mod tests {
|
|||||||
100
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export const MOCK_DASHBOARD_STATS: DashboardStatsResponse = {
|
|||||||
cache_read_tokens: 200000,
|
cache_read_tokens: 200000,
|
||||||
cache_creation_cost: 0.25,
|
cache_creation_cost: 0.25,
|
||||||
cache_read_cost: 0.10,
|
cache_read_cost: 0.10,
|
||||||
cache_hit_rate: 0.35,
|
cache_hit_rate: 35.0,
|
||||||
total_cache_tokens: 250000
|
total_cache_tokens: 250000
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
|
|||||||
Reference in New Issue
Block a user