mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(dashboard): 修复仪表盘与明细统计数值不一致并重建 cost_savings 聚合
- 统一 dashboard 聚合与 raw 查询的 token 计算,拆分今日节省和周期节省 - cache savings 改用 input price 估算未命中成本,修复历史 cost_savings 偏高 - raw 查询 total_tokens 改用 effective_input + output + cache_creation + cache_read 公式 - 新增独立 backfill 重建历史 cost_savings 聚合表,保留已发布 backfill 不变
This commit is contained in:
@@ -1005,7 +1005,19 @@ async fn dashboard_admin_hourly_stats_aggregate_payload(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc);
|
||||
let offset = chrono::Duration::minutes(i64::from(range.tz_offset_minutes));
|
||||
let today = dashboard_user_today(range.tz_offset_minutes);
|
||||
let Some(today_start_local) = today.and_hms_opt(0, 0, 0) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(today_start_naive_utc) = today_start_local.checked_sub_signed(offset) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let today_start_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||
today_start_naive_utc,
|
||||
chrono::Utc,
|
||||
);
|
||||
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc).min(today_start_utc);
|
||||
if range_start_utc >= aggregate_end_utc {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1126,7 +1138,19 @@ async fn dashboard_user_hourly_stats_aggregate_payload(
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc);
|
||||
let offset = chrono::Duration::minutes(i64::from(range.tz_offset_minutes));
|
||||
let today = dashboard_user_today(range.tz_offset_minutes);
|
||||
let Some(today_start_local) = today.and_hms_opt(0, 0, 0) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(today_start_naive_utc) = today_start_local.checked_sub_signed(offset) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let today_start_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||
today_start_naive_utc,
|
||||
chrono::Utc,
|
||||
);
|
||||
let aggregate_end_utc = range_end_exclusive_utc.min(cutoff_utc).min(today_start_utc);
|
||||
if range_start_utc >= aggregate_end_utc {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1669,7 +1693,18 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
/ today_totals.requests as f64
|
||||
* 100.0
|
||||
};
|
||||
let cost_savings =
|
||||
let today_cost_savings =
|
||||
match dashboard_load_cache_savings(state, today_range, user_filter).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("dashboard today cache savings lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let period_cost_savings =
|
||||
match dashboard_load_cache_savings(state, summary_range, user_filter).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
@@ -1696,7 +1731,7 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
{
|
||||
"name": "今日费用",
|
||||
"value": dashboard_format_usd(today_totals.total_cost_usd),
|
||||
"subValue": format!("节省 {}", dashboard_format_usd(cost_savings.max(0.0))),
|
||||
"subValue": format!("节省 {}", dashboard_format_usd(today_cost_savings.max(0.0))),
|
||||
"icon": "DollarSign",
|
||||
},
|
||||
{
|
||||
@@ -1726,7 +1761,7 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
"cost_stats": {
|
||||
"total_cost": dashboard_round_f64(period_totals.total_cost_usd, 4),
|
||||
"total_actual_cost": dashboard_round_f64(period_totals.actual_total_cost_usd, 4),
|
||||
"cost_savings": cost_savings,
|
||||
"cost_savings": period_cost_savings,
|
||||
},
|
||||
"cache_stats": cache_stats,
|
||||
"users": {
|
||||
|
||||
@@ -1521,8 +1521,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -1587,8 +1587,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -1663,8 +1663,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -1740,8 +1740,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -3216,8 +3216,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -3298,8 +3298,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -3384,8 +3384,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
@@ -3471,8 +3471,8 @@ WITH aggregated AS (
|
||||
COALESCE(
|
||||
SUM(
|
||||
COALESCE(
|
||||
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.output_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage_settlement_snapshots.input_price_per_1m AS DOUBLE PRECISION),
|
||||
CAST(usage.input_price_per_1m AS DOUBLE PRECISION),
|
||||
0
|
||||
) * GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)::DOUBLE PRECISION
|
||||
/ 1000000.0
|
||||
|
||||
@@ -54,7 +54,7 @@ SELECT
|
||||
COALESCE(SUM(input_tokens), 0)::BIGINT AS input_tokens,
|
||||
COALESCE(SUM(effective_input_tokens), 0)::BIGINT AS effective_input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0)::BIGINT AS output_tokens,
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(effective_input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(cache_creation_tokens), 0)::BIGINT AS cache_creation_tokens,
|
||||
COALESCE(SUM(cache_read_tokens), 0)::BIGINT AS cache_read_tokens,
|
||||
COALESCE(SUM(total_input_context), 0)::BIGINT AS total_input_context,
|
||||
@@ -85,7 +85,7 @@ SELECT
|
||||
COALESCE(SUM(input_tokens), 0)::BIGINT AS input_tokens,
|
||||
COALESCE(SUM(effective_input_tokens), 0)::BIGINT AS effective_input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0)::BIGINT AS output_tokens,
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(effective_input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(cache_creation_tokens), 0)::BIGINT AS cache_creation_tokens,
|
||||
COALESCE(SUM(cache_read_tokens), 0)::BIGINT AS cache_read_tokens,
|
||||
COALESCE(SUM(total_input_context), 0)::BIGINT AS total_input_context,
|
||||
@@ -196,8 +196,7 @@ pub(crate) async fn list_admin_dashboard_daily_totals_aggregates(
|
||||
SELECT
|
||||
date,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
COALESCE(total_cost, 0)::DOUBLE PRECISION AS total_cost,
|
||||
response_time_sum_ms,
|
||||
response_time_samples
|
||||
@@ -220,19 +219,16 @@ ORDER BY date ASC
|
||||
let date = row
|
||||
.try_get::<DateTime<Utc>, _>("date")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?;
|
||||
let input_tokens = row
|
||||
.try_get::<i64, _>("input_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?;
|
||||
let output_tokens = row
|
||||
.try_get::<i64, _>("output_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?;
|
||||
items.push(DashboardDailyTotalsAggregateRow {
|
||||
date: date.date_naive().to_string(),
|
||||
requests: row
|
||||
.try_get::<i32, _>("total_requests")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_tokens: input_tokens.saturating_add(output_tokens).max(0) as u64,
|
||||
total_tokens: row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_cost_usd: row
|
||||
.try_get::<f64, _>("total_cost")
|
||||
.map_err(|err| internal(format!("daily aggregate totals decode failed: {err}")))?,
|
||||
@@ -260,7 +256,7 @@ pub(crate) async fn list_admin_dashboard_hourly_totals_aggregates(
|
||||
SELECT
|
||||
CAST(DATE(hour_utc + ($3::integer * INTERVAL '1 minute')) AS TEXT) AS date,
|
||||
COALESCE(SUM(total_requests), 0)::BIGINT AS total_requests,
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0)::BIGINT AS total_tokens,
|
||||
CAST(COALESCE(SUM(total_cost), 0) AS DOUBLE PRECISION) AS total_cost,
|
||||
CAST(COALESCE(SUM(response_time_sum_ms), 0) AS DOUBLE PRECISION) AS response_time_sum_ms,
|
||||
COALESCE(SUM(response_time_samples), 0)::BIGINT AS response_time_samples
|
||||
@@ -321,8 +317,7 @@ SELECT
|
||||
date,
|
||||
model,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
COALESCE(total_cost, 0)::DOUBLE PRECISION AS total_cost,
|
||||
response_time_sum_ms,
|
||||
response_time_samples
|
||||
@@ -345,12 +340,6 @@ ORDER BY date ASC, total_cost DESC, model ASC
|
||||
let date = row
|
||||
.try_get::<DateTime<Utc>, _>("date")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?;
|
||||
let input_tokens = row
|
||||
.try_get::<i64, _>("input_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?;
|
||||
let output_tokens = row
|
||||
.try_get::<i64, _>("output_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?;
|
||||
items.push(DashboardDailyModelAggregateRow {
|
||||
date: date.date_naive().to_string(),
|
||||
model: row
|
||||
@@ -360,7 +349,10 @@ ORDER BY date ASC, total_cost DESC, model ASC
|
||||
.try_get::<i32, _>("total_requests")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_tokens: input_tokens.saturating_add(output_tokens).max(0) as u64,
|
||||
total_tokens: row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_cost_usd: row
|
||||
.try_get::<f64, _>("total_cost")
|
||||
.map_err(|err| internal(format!("daily aggregate model decode failed: {err}")))?,
|
||||
@@ -453,8 +445,7 @@ SELECT
|
||||
date,
|
||||
provider_name,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
COALESCE(total_cost, 0)::DOUBLE PRECISION AS total_cost
|
||||
FROM stats_daily_provider
|
||||
WHERE date >= $1
|
||||
@@ -475,12 +466,6 @@ ORDER BY date ASC, total_cost DESC, provider_name ASC
|
||||
let date = row
|
||||
.try_get::<DateTime<Utc>, _>("date")
|
||||
.map_err(|err| internal(format!("daily aggregate provider decode failed: {err}")))?;
|
||||
let input_tokens = row
|
||||
.try_get::<i64, _>("input_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate provider decode failed: {err}")))?;
|
||||
let output_tokens = row
|
||||
.try_get::<i64, _>("output_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate provider decode failed: {err}")))?;
|
||||
items.push(DashboardDailyProviderAggregateRow {
|
||||
date: date.date_naive().to_string(),
|
||||
provider: row.try_get::<String, _>("provider_name").map_err(|err| {
|
||||
@@ -490,7 +475,10 @@ ORDER BY date ASC, total_cost DESC, provider_name ASC
|
||||
.try_get::<i32, _>("total_requests")
|
||||
.map_err(|err| internal(format!("daily aggregate provider decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_tokens: input_tokens.saturating_add(output_tokens).max(0) as u64,
|
||||
total_tokens: row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
.map_err(|err| internal(format!("daily aggregate provider decode failed: {err}")))?
|
||||
.max(0) as u64,
|
||||
total_cost_usd: row.try_get::<f64, _>("total_cost").map_err(|err| {
|
||||
internal(format!("daily aggregate provider decode failed: {err}"))
|
||||
})?,
|
||||
@@ -567,8 +555,7 @@ pub(crate) async fn list_user_dashboard_daily_totals_aggregates(
|
||||
SELECT
|
||||
date,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
COALESCE(total_cost, 0)::DOUBLE PRECISION AS total_cost,
|
||||
response_time_sum_ms,
|
||||
response_time_samples
|
||||
@@ -593,12 +580,6 @@ ORDER BY date ASC
|
||||
let date = row
|
||||
.try_get::<DateTime<Utc>, _>("date")
|
||||
.map_err(|err| internal(format!("user daily aggregate totals decode failed: {err}")))?;
|
||||
let input_tokens = row
|
||||
.try_get::<i64, _>("input_tokens")
|
||||
.map_err(|err| internal(format!("user daily aggregate totals decode failed: {err}")))?;
|
||||
let output_tokens = row
|
||||
.try_get::<i64, _>("output_tokens")
|
||||
.map_err(|err| internal(format!("user daily aggregate totals decode failed: {err}")))?;
|
||||
items.push(DashboardDailyTotalsAggregateRow {
|
||||
date: date.date_naive().to_string(),
|
||||
requests: row
|
||||
@@ -607,7 +588,12 @@ ORDER BY date ASC
|
||||
internal(format!("user daily aggregate totals decode failed: {err}"))
|
||||
})?
|
||||
.max(0) as u64,
|
||||
total_tokens: input_tokens.saturating_add(output_tokens).max(0) as u64,
|
||||
total_tokens: row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
.map_err(|err| {
|
||||
internal(format!("user daily aggregate totals decode failed: {err}"))
|
||||
})?
|
||||
.max(0) as u64,
|
||||
total_cost_usd: row.try_get::<f64, _>("total_cost").map_err(|err| {
|
||||
internal(format!("user daily aggregate totals decode failed: {err}"))
|
||||
})?,
|
||||
@@ -640,7 +626,7 @@ pub(crate) async fn list_user_dashboard_hourly_totals_aggregates(
|
||||
SELECT
|
||||
CAST(DATE(hour_utc + ($4::integer * INTERVAL '1 minute')) AS TEXT) AS date,
|
||||
COALESCE(SUM(total_requests), 0)::BIGINT AS total_requests,
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0)::BIGINT AS total_tokens,
|
||||
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0)::BIGINT AS total_tokens,
|
||||
CAST(COALESCE(SUM(total_cost), 0) AS DOUBLE PRECISION) AS total_cost,
|
||||
CAST(COALESCE(SUM(response_time_sum_ms), 0) AS DOUBLE PRECISION) AS response_time_sum_ms,
|
||||
COALESCE(SUM(response_time_samples), 0)::BIGINT AS response_time_samples
|
||||
@@ -712,8 +698,7 @@ SELECT
|
||||
date,
|
||||
model,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
effective_input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
COALESCE(total_cost, 0)::DOUBLE PRECISION AS total_cost,
|
||||
response_time_sum_ms,
|
||||
response_time_samples
|
||||
@@ -738,12 +723,6 @@ ORDER BY date ASC, total_cost DESC, model ASC
|
||||
let date = row
|
||||
.try_get::<DateTime<Utc>, _>("date")
|
||||
.map_err(|err| internal(format!("user daily aggregate model decode failed: {err}")))?;
|
||||
let input_tokens = row
|
||||
.try_get::<i64, _>("input_tokens")
|
||||
.map_err(|err| internal(format!("user daily aggregate model decode failed: {err}")))?;
|
||||
let output_tokens = row
|
||||
.try_get::<i64, _>("output_tokens")
|
||||
.map_err(|err| internal(format!("user daily aggregate model decode failed: {err}")))?;
|
||||
items.push(DashboardDailyModelAggregateRow {
|
||||
date: date.date_naive().to_string(),
|
||||
model: row.try_get::<String, _>("model").map_err(|err| {
|
||||
@@ -755,7 +734,12 @@ ORDER BY date ASC, total_cost DESC, model ASC
|
||||
internal(format!("user daily aggregate model decode failed: {err}"))
|
||||
})?
|
||||
.max(0) as u64,
|
||||
total_tokens: input_tokens.saturating_add(output_tokens).max(0) as u64,
|
||||
total_tokens: row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
.map_err(|err| {
|
||||
internal(format!("user daily aggregate model decode failed: {err}"))
|
||||
})?
|
||||
.max(0) as u64,
|
||||
total_cost_usd: row.try_get::<f64, _>("total_cost").map_err(|err| {
|
||||
internal(format!("user daily aggregate model decode failed: {err}"))
|
||||
})?,
|
||||
|
||||
@@ -17,6 +17,7 @@ use axum::routing::{any, get};
|
||||
use axum::{extract::Request, Router};
|
||||
use chrono::Utc;
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::{
|
||||
build_router_with_state, sample_currently_usable_auth_snapshot, sample_provider, start_server,
|
||||
@@ -1235,6 +1236,7 @@ async fn gateway_handles_admin_stats_cost_savings_locally_with_trusted_admin_pri
|
||||
usage_row.cache_creation_cost_usd = 0.001;
|
||||
usage_row.cache_read_cost_usd = 0.002;
|
||||
usage_row.output_price_per_1m = Some(50.0);
|
||||
usage_row.request_metadata = Some(json!({ "input_price_per_1m": 30.0 }));
|
||||
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![usage_row]));
|
||||
|
||||
@@ -1261,8 +1263,8 @@ async fn gateway_handles_admin_stats_cost_savings_locally_with_trusted_admin_pri
|
||||
assert_eq!(payload["cache_read_tokens"], 100);
|
||||
assert_eq!(payload["cache_read_cost"], 0.002);
|
||||
assert_eq!(payload["cache_creation_cost"], 0.001);
|
||||
assert_eq!(payload["estimated_full_cost"], 0.005);
|
||||
assert_eq!(payload["cache_savings"], 0.003);
|
||||
assert_eq!(payload["estimated_full_cost"], 0.003);
|
||||
assert_eq!(payload["cache_savings"], 0.001);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -321,6 +321,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
openai_usage.cache_read_input_tokens = 800;
|
||||
openai_usage.cache_read_cost_usd = 0.01;
|
||||
openai_usage.output_price_per_1m = Some(100.0);
|
||||
openai_usage.request_metadata = Some(json!({ "input_price_per_1m": 20.0 }));
|
||||
|
||||
let mut claude_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-admin-2",
|
||||
@@ -342,6 +343,27 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
claude_usage.cache_read_input_tokens = 200;
|
||||
claude_usage.cache_read_cost_usd = 0.005;
|
||||
claude_usage.output_price_per_1m = Some(100.0);
|
||||
claude_usage.request_metadata = Some(json!({ "input_price_per_1m": 20.0 }));
|
||||
|
||||
let mut prior_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-admin-4",
|
||||
"req-dashboard-admin-4",
|
||||
"user-auth-1",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::days(1),
|
||||
);
|
||||
prior_usage.input_tokens = 2_000;
|
||||
prior_usage.output_tokens = 500;
|
||||
prior_usage.total_tokens = 2_500;
|
||||
prior_usage.cache_creation_input_tokens = 0;
|
||||
prior_usage.cache_creation_ephemeral_5m_input_tokens = 0;
|
||||
prior_usage.cache_creation_ephemeral_1h_input_tokens = 0;
|
||||
prior_usage.cache_read_input_tokens = 1_000;
|
||||
prior_usage.cache_read_cost_usd = 0.01;
|
||||
prior_usage.output_price_per_1m = Some(100.0);
|
||||
prior_usage.request_metadata = Some(json!({ "input_price_per_1m": 30.0 }));
|
||||
|
||||
let mut streaming_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-admin-3",
|
||||
@@ -358,6 +380,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||
openai_usage,
|
||||
claude_usage,
|
||||
prior_usage,
|
||||
streaming_usage,
|
||||
]));
|
||||
let user_repository = Arc::new(
|
||||
@@ -487,7 +510,11 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
.await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("{gateway_url}/api/dashboard/stats"))
|
||||
.get(format!(
|
||||
"{gateway_url}/api/dashboard/stats?start_date={}&end_date={}",
|
||||
(now - chrono::Duration::days(1)).date_naive(),
|
||||
now.date_naive(),
|
||||
))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-dashboard-stats-admin")
|
||||
.header("user-agent", "AetherTest/1.0")
|
||||
@@ -500,8 +527,8 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
assert_eq!(payload["today"]["requests"], 2);
|
||||
assert_eq!(payload["today"]["tokens"], 17_450);
|
||||
assert_eq!(payload["today"]["cost"], json!(2.5));
|
||||
assert_eq!(payload["cost_stats"]["cost_savings"], json!(0.085));
|
||||
assert_eq!(payload["stats"][2]["subValue"], json!("节省 $0.09"));
|
||||
assert_eq!(payload["cost_stats"]["cost_savings"], json!(0.025));
|
||||
assert_eq!(payload["stats"][2]["subValue"], json!("节省 $0.01"));
|
||||
assert_eq!(payload["stats"][0]["value"], json!("2"));
|
||||
assert_eq!(payload["stats"][1]["value"], json!("17.4K"));
|
||||
assert_eq!(
|
||||
@@ -561,34 +588,40 @@ async fn gateway_handles_dashboard_daily_stats_locally_without_proxying_upstream
|
||||
"refresh-dashboard-daily-stats",
|
||||
now,
|
||||
);
|
||||
let mut today_openai_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-daily-1",
|
||||
"req-dashboard-daily-1",
|
||||
"user-auth-1",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(1),
|
||||
);
|
||||
today_openai_usage.total_tokens = 160;
|
||||
let mut today_claude_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-daily-2",
|
||||
"req-dashboard-daily-2",
|
||||
"user-auth-2",
|
||||
"claude-3-7",
|
||||
"claude",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(2),
|
||||
);
|
||||
today_claude_usage.total_tokens = 160;
|
||||
let mut prior_usage = sample_user_usage_audit(
|
||||
"usage-dashboard-daily-3",
|
||||
"req-dashboard-daily-3",
|
||||
"user-auth-3",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::days(1) - chrono::Duration::hours(2),
|
||||
);
|
||||
prior_usage.total_tokens = 160;
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-daily-1",
|
||||
"req-dashboard-daily-1",
|
||||
"user-auth-1",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(1),
|
||||
),
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-daily-2",
|
||||
"req-dashboard-daily-2",
|
||||
"user-auth-2",
|
||||
"claude-3-7",
|
||||
"claude",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(2),
|
||||
),
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-daily-3",
|
||||
"req-dashboard-daily-3",
|
||||
"user-auth-3",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::days(1) - chrono::Duration::hours(2),
|
||||
),
|
||||
today_openai_usage,
|
||||
today_claude_usage,
|
||||
prior_usage,
|
||||
]));
|
||||
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
@@ -636,6 +669,7 @@ async fn gateway_handles_dashboard_daily_stats_locally_without_proxying_upstream
|
||||
assert_eq!(daily_stats[0]["unique_providers"], 1);
|
||||
assert_eq!(daily_stats[1]["date"], json!(now.date_naive().to_string()));
|
||||
assert_eq!(daily_stats[1]["requests"], 2);
|
||||
assert_eq!(daily_stats[1]["tokens"], 320);
|
||||
assert_eq!(daily_stats[1]["unique_models"], 2);
|
||||
assert_eq!(daily_stats[1]["unique_providers"], 2);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user