fix(dashboard): 使用有效输入口径展示今日 Token (#294)

将仪表盘“今日 Token”卡片中的输入项改为有效输入口径。
对 OpenAI/Gemini 从 input_tokens 中扣除 cache_read_tokens,
Claude 保持原始输入口径不变。

同步更新 dashboard 集成测试,覆盖 OpenAI 与 Claude 的差异化行为。
This commit is contained in:
AAEE86
2026-04-14 14:53:59 +08:00
committed by GitHub
parent 21ac1825f3
commit b70f32c6c2
2 changed files with 27 additions and 10 deletions

View File

@@ -2,7 +2,9 @@ use super::{
build_auth_error_response, query_param_value, resolve_authenticated_local_user, AppState,
GatewayError, GatewayPublicRequestContext,
};
use aether_billing::normalize_total_input_context_for_cache_hit_rate;
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 axum::{
body::Body,
@@ -25,6 +27,7 @@ struct DashboardDateRange {
struct DashboardUsageTotals {
requests: u64,
input_tokens: u64,
effective_input_tokens: u64,
output_tokens: u64,
total_tokens: u64,
cache_creation_tokens: u64,
@@ -74,6 +77,7 @@ impl DashboardUsageTotals {
let cache_creation_tokens = dashboard_cache_creation_tokens(item);
self.requests += 1;
self.input_tokens += item.input_tokens;
self.effective_input_tokens += dashboard_effective_input_tokens(item);
self.output_tokens += item.output_tokens;
self.total_tokens += item.total_tokens;
self.cache_creation_tokens += cache_creation_tokens;
@@ -150,6 +154,16 @@ fn dashboard_total_input_context(item: &StoredRequestUsageAudit) -> u64 {
) as u64
}
fn dashboard_effective_input_tokens(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_read_tokens = i64::try_from(item.cache_read_input_tokens).unwrap_or(i64::MAX);
normalize_input_tokens_for_billing(api_format, input_tokens, cache_read_tokens) as u64
}
fn dashboard_round_f64(value: f64, decimals: u32) -> f64 {
let factor = 10_f64.powi(i32::try_from(decimals).unwrap_or_default());
(value * factor).round() / factor
@@ -219,7 +233,7 @@ fn dashboard_format_token_subvalue(totals: &DashboardUsageTotals) -> String {
fn dashboard_format_today_token_subvalue(totals: &DashboardUsageTotals) -> String {
format!(
"输入 {} / 输出 {} · 写缓存 {} / 读缓存 {}",
dashboard_format_token_compact(totals.input_tokens),
dashboard_format_token_compact(totals.effective_input_tokens),
dashboard_format_token_compact(totals.output_tokens),
dashboard_format_token_compact(totals.cache_creation_tokens),
dashboard_format_token_compact(totals.cache_read_tokens)
@@ -602,7 +616,7 @@ pub(super) async fn handle_dashboard_stats_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard api key stats lookup failed: {err:?}"),
false,
)
);
}
};
@@ -638,7 +652,7 @@ pub(super) async fn handle_dashboard_stats_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard user stats lookup failed: {err:?}"),
false,
)
);
}
};
let success_rate = if today_totals.requests == 0 {
@@ -725,7 +739,7 @@ pub(super) async fn handle_dashboard_stats_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard wallet lookup failed: {err:?}"),
false,
)
);
}
}
} else {
@@ -1049,7 +1063,7 @@ pub(super) async fn handle_dashboard_recent_requests_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard recent requests lookup failed: {err:?}"),
false,
)
);
}
};
@@ -1067,7 +1081,7 @@ pub(super) async fn handle_dashboard_recent_requests_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard user lookup failed: {err:?}"),
false,
)
);
}
};
let mut usernames_by_id: BTreeMap<String, String> = users_by_id
@@ -1127,7 +1141,7 @@ pub(super) async fn handle_dashboard_provider_status_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard provider catalog lookup failed: {err:?}"),
false,
)
);
}
};
let since_unix_secs = u64::try_from(chrono::Utc::now().timestamp())
@@ -1149,7 +1163,7 @@ pub(super) async fn handle_dashboard_provider_status_get(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("dashboard provider usage lookup failed: {err:?}"),
false,
)
);
}
};

View File

@@ -158,6 +158,7 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
assert_eq!(payload["today"]["tokens"], 150);
assert_eq!(payload["api_keys"]["total"], 2);
assert_eq!(payload["api_keys"]["active"], 1);
assert_eq!(payload["stats"][3]["subValue"], json!("输入 240 / 输出 60"));
assert_eq!(payload["token_breakdown"]["input"], 240);
assert_eq!(payload["token_breakdown"]["output"], 60);
assert_eq!(payload["token_breakdown"]["cache_creation"], 20);
@@ -242,6 +243,8 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
claude_usage.input_tokens = 900;
claude_usage.output_tokens = 100;
claude_usage.total_tokens = 1_000;
claude_usage.api_format = Some("claude:chat".to_string());
claude_usage.endpoint_api_format = Some("claude:chat".to_string());
claude_usage.cache_creation_input_tokens = 50;
claude_usage.cache_creation_ephemeral_5m_input_tokens = 20;
claude_usage.cache_creation_ephemeral_1h_input_tokens = 30;
@@ -403,7 +406,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
assert_eq!(payload["stats"][1]["value"], json!("16K"));
assert_eq!(
payload["stats"][1]["subValue"],
json!("输入 12.9K / 输出 3.1K · 写缓存 1.25K / 读缓存 1K")
json!("输入 12.1K / 输出 3.1K · 写缓存 1.25K / 读缓存 1K")
);
assert_eq!(payload["users"]["total"], 2);
assert_eq!(payload["users"]["active"], 1);