Files
codex-proxy-rs/backend/tests/api/admin/monitoring/dashboard.rs
T
2026-07-10 19:35:58 +08:00

837 lines
26 KiB
Rust

use axum::{
body::Body,
http::{Request, StatusCode},
};
use chrono::{Duration, Utc};
use codex_proxy_rs::{
bootstrap::{services::Services, state::AppState},
infra::time::{china_datetime, china_day_start, china_hour},
telemetry::{usage::store::PgUsageRecordStore, usage::types::UsageRecord},
upstream::openai::fingerprint::Fingerprint,
};
use serde_json::{json, Value};
use sqlx::PgPool;
use tower::util::ServiceExt;
use crate::support::{
admin::seed_admin_session,
fingerprint::runtime_fingerprint,
http::response_json,
storage::{background_task_stores, create_test_redis, init_test_db, test_database_url},
};
#[tokio::test]
async fn dashboard_summary_should_render_fingerprint_updated_at_as_beijing_time() {
let (app, _store, _pool, _dir) = dashboard_test_app(
"dashboard-fingerprint-updated",
crate::support::fingerprint::test_fingerprint_with_updated_at(Some(
"2026-06-23T11:36:46.965574590+00:00",
)),
)
.await;
let body = dashboard_summary(app).await;
assert_eq!(
service_status_value(&body, "更新时间"),
"2026-06-23 19:36:46"
);
}
#[tokio::test]
async fn dashboard_summary_should_render_dash_when_fingerprint_updated_at_is_missing() {
let (app, _store, _pool, _dir) = dashboard_test_app(
"dashboard-fingerprint-missing",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let body = dashboard_summary(app).await;
assert_eq!(service_status_value(&body, "更新时间"), "-");
}
#[tokio::test]
async fn dashboard_summary_should_calculate_today_traffic_and_average_first_token_latency_from_time_buckets(
) {
let (app, store, pool, _dir) = dashboard_test_app(
"dashboard-traffic-latency",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let now = Utc::now();
let today_start = china_day_start(now);
let today_record_at = if now - today_start > Duration::minutes(1) {
today_start + Duration::minutes(1)
} else {
now - Duration::seconds(1)
};
let yesterday_record_at = today_start - Duration::minutes(1);
let mut first = usage_record_with_tokens(today_record_at, 10);
first.latency_ms = Some(1_000);
first.first_token_ms = Some(200);
let mut second = usage_record_with_tokens(today_record_at, 20);
second.latency_ms = Some(2_000);
second.first_token_ms = Some(400);
store.append(&first).await.unwrap();
store.append(&second).await.unwrap();
store
.append(&usage_record_with_tokens(yesterday_record_at, 30))
.await
.unwrap();
seed_lifetime_usage_summary(&pool, 7, 400, 500, 100).await;
let body = dashboard_summary(app).await;
assert_eq!(body["data"]["cards"]["traffic"]["todayRequests"], "2");
assert_eq!(body["data"]["cards"]["traffic"]["todayRequestsValue"], 2);
assert_eq!(body["data"]["cards"]["traffic"]["totalRequests"], "7");
assert_eq!(
body["data"]["cards"]["traffic"]["yesterdayRequestsValue"],
1
);
assert_eq!(body["data"]["cards"]["tokens"]["todayTokens"], "30");
assert_eq!(body["data"]["cards"]["tokens"]["todayTokensValue"], 30);
assert_eq!(body["data"]["cards"]["tokens"]["totalTokens"], "900");
assert_eq!(body["data"]["cards"]["tokens"]["yesterdayTokensValue"], 30);
assert_eq!(body["data"]["cards"]["cache"]["totalHitRate"], "25.0%");
assert_eq!(body["data"]["cards"]["cache"]["totalCachedTokens"], "100");
assert_eq!(
body["data"]["cards"]["cache"]["averageFirstTokenLatencyMs"],
"300 ms"
);
assert!(body["data"]["cards"]["cache"]
.as_object()
.unwrap()
.get("averageLatencyMs")
.is_none());
}
#[tokio::test]
async fn dashboard_summary_should_keep_trend_after_usage_records_are_cleared() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-time-buckets-survive-usage-record-clear",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let mut record = usage_record_with_tokens(Utc::now(), 12);
record.latency_ms = Some(900);
store.append(&record).await.unwrap();
store.clear().await.unwrap();
let body = dashboard_summary(app).await;
assert_eq!(body["data"]["usageRecords"].as_array().unwrap().len(), 0);
assert_eq!(body["data"]["cards"]["traffic"]["todayRequests"], "1");
assert_eq!(body["data"]["cards"]["tokens"]["todayTokens"], "12");
assert_eq!(
body["data"]["trend"]["points"]
.as_array()
.unwrap()
.iter()
.map(|point| point["requestsValue"].as_u64().unwrap())
.sum::<u64>(),
1
);
}
#[tokio::test]
async fn dashboard_trend_should_bucket_usage_by_china_hour() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-trend-china-hour",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let now = Utc::now();
store
.append(&usage_record_with_token_parts(
now - Duration::seconds(1),
40,
60,
10,
))
.await
.unwrap();
let body = dashboard_trend(app, "usage").await;
let point = body["data"]["points"]
.as_array()
.unwrap()
.iter()
.find(|point| point["requestsValue"] == 1)
.expect("trend should include the inserted usage bucket");
assert_eq!(point["time"], format!("{:02}", china_hour(&now)));
assert_eq!(point["tokensValue"], 100);
assert_eq!(point["cachedTokensValue"], 10);
assert_f64_eq(point["cacheHitRateValue"].as_f64().unwrap(), 0.25);
}
#[tokio::test]
async fn dashboard_trend_should_only_include_china_today() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-trend-china-today",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let now = Utc::now();
let today_start = china_day_start(now);
store
.append(&usage_record_with_tokens(
today_start - Duration::hours(1),
99,
))
.await
.unwrap();
store
.append(&usage_record_with_tokens(now, 7))
.await
.unwrap();
let body = dashboard_trend(app, "usage").await;
let points = body["data"]["points"].as_array().unwrap();
let token_total = points
.iter()
.map(|point| point["tokensValue"].as_u64().unwrap())
.sum::<u64>();
assert_eq!(points.first().unwrap()["time"], "00");
assert_eq!(token_total, 7);
}
#[tokio::test]
async fn dashboard_latency_trend_should_use_first_token_latency() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-trend-first-token-latency",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let now = Utc::now();
let mut slow_completion = usage_record_with_tokens(now, 10);
slow_completion.latency_ms = Some(10_000);
slow_completion.first_token_ms = Some(100);
slow_completion.service_tier = Some("default".to_string());
let mut fast_completion = usage_record_with_tokens(now, 10);
fast_completion.latency_ms = Some(1_000);
fast_completion.first_token_ms = Some(500);
fast_completion.service_tier = Some("priority".to_string());
store.append(&slow_completion).await.unwrap();
store.append(&fast_completion).await.unwrap();
let body = dashboard_trend(app, "latency").await;
let point = body["data"]["points"]
.as_array()
.unwrap()
.iter()
.find(|point| point["requestsValue"] == 2)
.expect("latency trend should include the inserted usage bucket");
let summary = body["data"]["summary"].as_array().unwrap();
assert_eq!(body["data"]["kind"], "latency");
assert_eq!(point["latencyValue"], 300);
assert_eq!(point["latency"], "300 ms");
assert_eq!(point["maxLatencyValue"], 500);
assert_eq!(point["maxLatency"], "500 ms");
assert_eq!(point["minLatencyValue"], 100);
assert_eq!(point["minLatency"], "100 ms");
assert_eq!(summary[0]["value"], "300 ms");
assert_eq!(summary[1]["value"], "500 ms");
assert_eq!(summary[2]["value"], "100 ms");
}
#[tokio::test]
async fn dashboard_summary_should_return_requested_trend_kind() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-summary-requested-trend-kind",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let now = Utc::now();
let mut record = usage_record_with_tokens(now - Duration::seconds(1), 10);
record.latency_ms = Some(10_000);
record.first_token_ms = Some(250);
store.append(&record).await.unwrap();
let body = dashboard_summary_with_kind(app, "latency").await;
assert_eq!(body["data"]["trend"]["kind"], "latency");
assert!(
body["data"]["trend"]["summary"]
.as_array()
.unwrap()
.iter()
.any(|item| item["value"] == "250 ms"),
"summary should include first-token latency trend values: {body:?}"
);
}
#[tokio::test]
async fn dashboard_summary_should_calculate_priority_cost_from_event_service_tier() {
let cost = dashboard_summary_today_cost_for_usage_record(
"dashboard-cost-priority",
"gpt-5.5",
json!({
"usage": {
"inputTokens": 100_000u64,
"outputTokens": 100_000u64,
"cachedTokens": 0u64
},
"serviceTier": "priority"
}),
)
.await;
assert_eq!(cost, "$8.75");
}
#[tokio::test]
async fn dashboard_summary_should_charge_cached_input_at_cache_read_price() {
let cost = dashboard_summary_today_cost_for_usage_record(
"dashboard-cost-cached",
"gpt-5.5",
json!({
"usage": {
"inputTokens": 1_000u64,
"outputTokens": 1_000u64,
"cachedTokens": 500u64
}
}),
)
.await;
assert_eq!(cost, "$0.03");
}
#[tokio::test]
async fn dashboard_summary_should_apply_long_context_prices() {
let cost = dashboard_summary_today_cost_for_usage_record(
"dashboard-cost-long-context",
"gpt-5.5",
json!({
"usage": {
"inputTokens": 300_000u64,
"outputTokens": 1_000u64,
"cachedTokens": 0u64
}
}),
)
.await;
assert_eq!(cost, "$3.04");
}
#[tokio::test]
async fn dashboard_summary_should_normalize_codex_model_names_for_cost() {
let cost = dashboard_summary_today_cost_for_usage_record(
"dashboard-cost-codex-model",
"codex-auto-review",
json!({
"usage": {
"inputTokens": 100_000u64,
"outputTokens": 100_000u64,
"cachedTokens": 0u64
}
}),
)
.await;
assert_eq!(cost, "$1.75");
}
#[tokio::test]
async fn dashboard_summary_should_not_price_usage_summary_without_model_dimension() {
let (app, _store, pool, _dir) = dashboard_test_app(
"dashboard-cost-summary-fallback",
crate::support::fingerprint::test_fingerprint(),
)
.await;
seed_usage_summary(&pool, 100_000, 100_000).await;
let body = dashboard_summary(app).await;
assert_eq!(body["data"]["cards"]["tokens"]["todayCostUsd"], "$0.00");
}
#[tokio::test]
async fn dashboard_summary_should_return_backend_formatted_time_fields() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-backend-formatted-time",
crate::support::fingerprint::test_fingerprint(),
)
.await;
let record_at = Utc::now() - Duration::seconds(1);
store
.append(&usage_record_with_tokens(record_at, 10))
.await
.unwrap();
let body = dashboard_summary(app).await;
assert_eq!(
body["data"]["usageRecords"][0]["createdAtDisplay"],
china_datetime(&record_at)
);
assert!(body["data"]["usageRecords"][0].get("metadata").is_none());
}
#[tokio::test]
async fn dashboard_summary_should_only_return_today_account_usage_and_usage_records() {
let (app, store, pool, _dir) = dashboard_test_app(
"dashboard-summary-today-records",
crate::support::fingerprint::test_fingerprint(),
)
.await;
seed_dashboard_account(&pool, "acct_today", "today@example.com").await;
seed_dashboard_account(&pool, "acct_yesterday", "yesterday@example.com").await;
let now = Utc::now();
let today_start = china_day_start(now);
let today_record_at = if now - today_start > Duration::minutes(1) {
today_start + Duration::minutes(1)
} else {
now - Duration::seconds(1)
};
let yesterday_record_at = today_start - Duration::minutes(1);
store
.append(&usage_record_with_account_tokens(
yesterday_record_at,
"acct_yesterday",
90,
))
.await
.unwrap();
store
.append(&usage_record_with_account_tokens(
today_record_at,
"acct_today",
7,
))
.await
.unwrap();
let body = dashboard_summary(app).await;
assert_eq!(body["data"]["usageRecords"].as_array().unwrap().len(), 1);
assert_eq!(
body["data"]["usageRecords"][0]["accountEmail"],
"today@example.com"
);
assert_eq!(body["data"]["accountUsage"].as_array().unwrap().len(), 1);
assert_eq!(
body["data"]["accountUsage"][0]["email"],
"today@example.com"
);
assert_eq!(body["data"]["accountUsage"][0]["tokens"], "7");
}
#[tokio::test]
async fn dashboard_summary_should_order_account_usage_by_recent_usage() {
let (app, store, pool, _dir) = dashboard_test_app(
"dashboard-account-usage-recent-order",
crate::support::fingerprint::test_fingerprint(),
)
.await;
seed_dashboard_account(&pool, "acct_old_heavy", "old-heavy@example.com").await;
seed_dashboard_account(&pool, "acct_recent_light", "recent-light@example.com").await;
let now = Utc::now();
store
.append(&usage_record_with_account_tokens(
now - Duration::minutes(2),
"acct_old_heavy",
2_600_000,
))
.await
.unwrap();
store
.append(&usage_record_with_account_tokens(
now - Duration::minutes(1),
"acct_recent_light",
660_000,
))
.await
.unwrap();
let body = dashboard_summary(app).await;
assert_eq!(
body["data"]["accountUsage"][0]["email"],
"recent-light@example.com"
);
assert_eq!(body["data"]["accountUsage"][0]["id"], "acct_recent_light");
assert_eq!(body["data"]["accountUsage"][0]["planType"], "K12");
assert_eq!(body["data"]["accountUsage"][0]["tokens"], "660K");
assert_eq!(
body["data"]["accountUsage"][1]["email"],
"old-heavy@example.com"
);
}
#[tokio::test]
async fn dashboard_summary_should_use_five_hour_quota_window_for_account_usage() {
let (app, store, pool, _dir) = dashboard_test_app(
"dashboard-account-usage-five-hour-quota",
crate::support::fingerprint::test_fingerprint(),
)
.await;
seed_dashboard_account(&pool, "acct_quota_five_hour", "quota-five-hour@example.com").await;
store
.append(&usage_record_with_account_tokens(
Utc::now() - Duration::seconds(1),
"acct_quota_five_hour",
1,
))
.await
.unwrap();
set_account_quota_json(
&pool,
"acct_quota_five_hour",
json!({
"monthly_limit": {
"used_percent": 92.0,
"window_minutes": 43_200
},
"snapshots": [
{
"source": "core",
"primary": {
"used_percent": 31.0,
"window_minutes": 300
},
"secondary": {
"used_percent": 88.0,
"window_minutes": 10_080
}
}
]
}),
)
.await;
let body = dashboard_summary(app).await;
assert_f64_eq(
body["data"]["accountUsage"][0]["quotaUsedPercent"]
.as_f64()
.unwrap(),
31.0,
);
}
#[tokio::test]
async fn dashboard_summary_should_fall_back_to_weekly_quota_window_when_five_hour_missing() {
let (app, store, pool, _dir) = dashboard_test_app(
"dashboard-account-usage-weekly-quota",
crate::support::fingerprint::test_fingerprint(),
)
.await;
seed_dashboard_account(&pool, "acct_quota_weekly", "quota-weekly@example.com").await;
store
.append(&usage_record_with_account_tokens(
Utc::now() - Duration::seconds(1),
"acct_quota_weekly",
1,
))
.await
.unwrap();
set_account_quota_json(
&pool,
"acct_quota_weekly",
json!({
"monthly_limit": {
"used_percent": 92.0,
"window_minutes": 43_200
},
"snapshots": [
{
"source": "experimental",
"primary": {
"used_percent": 99.0,
"window_minutes": 120
},
"secondary": {
"used_percent": 47.0,
"window_minutes": 10_080
}
}
]
}),
)
.await;
let body = dashboard_summary(app).await;
assert_f64_eq(
body["data"]["accountUsage"][0]["quotaUsedPercent"]
.as_f64()
.unwrap(),
47.0,
);
}
#[tokio::test]
async fn dashboard_summary_should_return_today_health_timeline() {
let (app, store, _pool, _dir) = dashboard_test_app(
"dashboard-health-timeline",
crate::support::fingerprint::test_fingerprint(),
)
.await;
store
.append(&usage_record_with_tokens(Utc::now(), 10))
.await
.unwrap();
store
.append(&usage_record_with_tokens(
Utc::now() - Duration::days(8),
10,
))
.await
.unwrap();
let body = dashboard_summary(app).await;
let points = body["data"]["healthTimeline"]["points"].as_str().unwrap();
assert_eq!(body["data"]["healthTimeline"]["title"], "请求健康时间线");
assert_eq!(body["data"]["healthTimeline"]["description"], "请求可靠性");
assert_eq!(points.len(), 96);
assert_eq!(points.matches('4').count(), 1);
}
async fn dashboard_summary_today_cost_for_usage_record(
db_name: &str,
model: &str,
metadata: Value,
) -> String {
let (app, store, _pool, _dir) =
dashboard_test_app(db_name, crate::support::fingerprint::test_fingerprint()).await;
let mut record = usage_record(Utc::now(), model, metadata.clone());
record.input_tokens = metadata
.pointer("/usage/inputTokens")
.and_then(Value::as_u64)
.map(|value| value as i64);
record.output_tokens = metadata
.pointer("/usage/outputTokens")
.and_then(Value::as_u64)
.map(|value| value as i64);
record.cached_tokens = metadata
.pointer("/usage/cachedTokens")
.and_then(Value::as_u64)
.map(|value| value as i64);
record.service_tier = metadata
.get("serviceTier")
.and_then(Value::as_str)
.map(str::to_string);
store.append(&record).await.unwrap();
let body = dashboard_summary(app).await;
body["data"]["cards"]["tokens"]["todayCostUsd"]
.as_str()
.unwrap()
.to_string()
}
async fn dashboard_test_app(
db_name: &str,
fingerprint: Fingerprint,
) -> (axum::Router, PgUsageRecordStore, PgPool, tempfile::TempDir) {
let (pool, dir) = init_test_db(db_name).await;
let redis = create_test_redis(db_name).await;
seed_admin_session(&pool, &redis, "session_1").await;
let config = crate::support::config::test_config(test_database_url());
let stores = background_task_stores(pool.clone(), redis);
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
(
codex_proxy_rs::api::router::router().with_state(state),
PgUsageRecordStore::new(pool.clone()),
pool,
dir,
)
}
async fn dashboard_summary(app: axum::Router) -> Value {
dashboard_summary_uri(app, "/api/admin/dashboard/summary").await
}
async fn dashboard_summary_with_kind(app: axum::Router, kind: &str) -> Value {
dashboard_summary_uri(app, &format!("/api/admin/dashboard/summary?kind={kind}")).await
}
async fn dashboard_summary_uri(app: axum::Router, uri: &str) -> Value {
let response = app
.oneshot(
Request::builder()
.method("GET")
.uri(uri)
.header("cookie", "cpr_admin_session=session_1")
.header("x-request-id", "req_dashboard_summary")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
response_json(response).await
}
async fn dashboard_trend(app: axum::Router, kind: &str) -> Value {
let response = app
.oneshot(
Request::builder()
.method("GET")
.uri(format!("/api/admin/dashboard/trend?kind={kind}"))
.header("cookie", "cpr_admin_session=session_1")
.header("x-request-id", "req_dashboard_trend")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
response_json(response).await
}
async fn seed_usage_summary(pool: &PgPool, input_tokens: u64, output_tokens: u64) {
seed_usage_summary_with_last_used(pool, input_tokens, output_tokens, "2026-06-18T00:00:00Z")
.await;
}
async fn seed_lifetime_usage_summary(
pool: &PgPool,
request_count: u64,
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
) {
seed_dashboard_account(pool, "acct_lifetime", "lifetime@example.com").await;
sqlx::query(
"insert into account_usage
(account_id, request_count, input_tokens, output_tokens, cached_tokens, total_tokens)
values ($1, $2, $3, $4, $5, $6)",
)
.bind("acct_lifetime")
.bind(request_count as i64)
.bind(input_tokens as i64)
.bind(output_tokens as i64)
.bind(cached_tokens as i64)
.bind((input_tokens + output_tokens) as i64)
.execute(pool)
.await
.unwrap();
}
async fn seed_usage_summary_with_last_used(
pool: &PgPool,
input_tokens: u64,
output_tokens: u64,
last_used_at: &str,
) {
sqlx::query(
"insert into accounts (id, email, access_token, status, added_at, updated_at) values ($1, $2, $3, $4, $5, $6)",
)
.bind("acct_usage")
.bind("acct-usage@example.com")
.bind("cipher")
.bind("active")
.bind(crate::support::storage::timestamp("2026-06-18T00:00:00Z"))
.bind(crate::support::storage::timestamp("2026-06-18T00:00:00Z"))
.execute(pool)
.await
.unwrap();
sqlx::query(
"insert into account_usage (account_id, request_count, input_tokens, output_tokens, total_tokens, last_used_at) values ($1, $2, $3, $4, $5, $6)",
)
.bind("acct_usage")
.bind(1_i64)
.bind(input_tokens as i64)
.bind(output_tokens as i64)
.bind((input_tokens + output_tokens) as i64)
.bind(crate::support::storage::timestamp(last_used_at))
.execute(pool)
.await
.unwrap();
}
async fn seed_dashboard_account(pool: &PgPool, account_id: &str, email: &str) {
sqlx::query(
"insert into accounts (id, email, chatgpt_account_id, plan_type, access_token, status, added_at, updated_at) values ($1, $2, $3, $4, $5, $6, $7, $8)",
)
.bind(account_id)
.bind(email)
.bind(format!("upstream_{account_id}"))
.bind("K12")
.bind("cipher")
.bind("active")
.bind(crate::support::storage::timestamp("2026-06-18T00:00:00Z"))
.bind(crate::support::storage::timestamp("2026-06-18T00:00:00Z"))
.execute(pool)
.await
.unwrap();
}
async fn set_account_quota_json(pool: &PgPool, account_id: &str, quota_json: Value) {
sqlx::query("update accounts set quota_json = $1::jsonb, quota_fetched_at = $2 where id = $3")
.bind(quota_json.to_string())
.bind(crate::support::storage::timestamp("2026-06-30T08:01:00Z"))
.bind(account_id)
.execute(pool)
.await
.unwrap();
}
fn service_status_value<'a>(body: &'a Value, label: &str) -> &'a str {
body["data"]["serviceStatuses"]
.as_array()
.unwrap()
.iter()
.find(|status| status["label"] == label)
.and_then(|status| status["value"].as_str())
.unwrap()
}
fn usage_record_with_tokens(created_at: chrono::DateTime<Utc>, total_tokens: u64) -> UsageRecord {
usage_record_with_token_parts(created_at, total_tokens, 0, 0)
}
fn usage_record_with_token_parts(
created_at: chrono::DateTime<Utc>,
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
) -> UsageRecord {
let mut record = usage_record(created_at, "gpt-5.5", json!({}));
record.input_tokens = Some(input_tokens as i64);
record.output_tokens = Some(output_tokens as i64);
record.cached_tokens = Some(cached_tokens as i64);
record
}
fn usage_record_with_account_tokens(
created_at: chrono::DateTime<Utc>,
account_id: &str,
total_tokens: u64,
) -> UsageRecord {
let mut record = usage_record_with_tokens(created_at, total_tokens);
record.account_id = account_id.to_string();
record
}
fn usage_record(created_at: chrono::DateTime<Utc>, model: &str, metadata: Value) -> UsageRecord {
let mut record = UsageRecord::new("v1.response", "completed", "acct_dashboard", model, 200);
record.created_at = created_at;
record.metadata = metadata;
record
}
fn assert_f64_eq(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-12,
"expected {expected}, got {actual}"
);
}