refactor(data): remove MySQL and SQLite support

Use PostgreSQL as the only database backend across runtime, schema tooling, installation, Compose, and CI. Update regression tests and reject removed drivers explicitly.
This commit is contained in:
elky
2026-09-07 00:09:42 +08:00
parent b5ed802277
commit 2281f2b754
298 changed files with 793 additions and 134804 deletions
@@ -1,831 +0,0 @@
use chrono::{DateTime, Utc};
use sqlx::Row;
use crate::backend::stats_common::{stats_id, unix_secs, utc_from_unix_secs};
use crate::backend::MysqlBackend;
use crate::driver::mysql::MysqlPool;
use crate::error::SqlResultExt;
use crate::{
DataLayerError, StatsDailyAggregationInput, StatsDailyAggregationSummary,
StatsHourlyAggregationInput, StatsHourlyAggregationSummary,
};
mod advanced;
impl MysqlBackend {
pub async fn aggregate_stats_hourly(
&self,
input: &StatsHourlyAggregationInput,
) -> Result<Option<StatsHourlyAggregationSummary>, DataLayerError> {
let Some(hour_utc_unix_secs) =
next_mysql_stats_hourly_bucket(self.pool(), input.target_hour_utc).await?
else {
return Ok(None);
};
perform_mysql_stats_hourly_aggregation(self.pool(), hour_utc_unix_secs, input.aggregated_at)
.await
.map(Some)
}
pub async fn aggregate_stats_daily(
&self,
input: &StatsDailyAggregationInput,
) -> Result<Option<StatsDailyAggregationSummary>, DataLayerError> {
let Some(day_start_unix_secs) =
next_mysql_stats_daily_bucket(self.pool(), input.target_day_utc).await?
else {
return Ok(None);
};
perform_mysql_stats_daily_aggregation(self.pool(), day_start_unix_secs, input.aggregated_at)
.await
.map(Some)
}
}
async fn next_mysql_stats_hourly_bucket(
pool: &MysqlPool,
target_hour_utc: DateTime<Utc>,
) -> Result<Option<i64>, DataLayerError> {
let latest_hour: Option<i64> =
sqlx::query_scalar("SELECT MAX(hour_utc) FROM stats_hourly WHERE is_complete <> 0")
.fetch_one(pool)
.await
.map_sql_err()?;
let search_from = latest_hour.map(|value| value + 3600).unwrap_or(0);
let search_until = unix_secs(target_hour_utc) + 3600;
if search_from >= search_until {
return Ok(None);
}
let next_bucket: Option<i64> = sqlx::query_scalar(
r#"
SELECT CAST(MIN(FLOOR(created_at_unix_ms / 3600) * 3600) AS SIGNED)
FROM `usage`
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#,
)
.bind(search_from)
.bind(search_until)
.fetch_one(pool)
.await
.map_sql_err()?;
Ok(next_bucket.filter(|value| *value <= unix_secs(target_hour_utc)))
}
async fn next_mysql_stats_daily_bucket(
pool: &MysqlPool,
target_day_utc: DateTime<Utc>,
) -> Result<Option<i64>, DataLayerError> {
let latest_day: Option<i64> =
sqlx::query_scalar("SELECT MAX(`date`) FROM stats_daily WHERE is_complete <> 0")
.fetch_one(pool)
.await
.map_sql_err()?;
let search_from = latest_day.map(|value| value + 86_400).unwrap_or(0);
let search_until = unix_secs(target_day_utc) + 86_400;
if search_from >= search_until {
return Ok(None);
}
let next_bucket: Option<i64> = sqlx::query_scalar(
r#"
SELECT CAST(MIN(FLOOR(created_at_unix_ms / 86400) * 86400) AS SIGNED)
FROM `usage`
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#,
)
.bind(search_from)
.bind(search_until)
.fetch_one(pool)
.await
.map_sql_err()?;
Ok(next_bucket.filter(|value| *value <= unix_secs(target_day_utc)))
}
const MYSQL_STATS_AGGREGATE_SQL: &str = r#"
SELECT
CAST(COUNT(*) AS SIGNED) AS total_requests,
CAST(COALESCE(SUM(CASE
WHEN status = 'failed'
OR status_code >= 400
OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0) AS SIGNED) AS error_requests,
CAST(COALESCE(SUM(input_tokens), 0) AS SIGNED) AS input_tokens,
CAST(COALESCE(SUM(output_tokens), 0) AS SIGNED) AS output_tokens,
CAST(COALESCE(SUM(cache_creation_input_tokens), 0) AS SIGNED) AS cache_creation_tokens,
CAST(COALESCE(SUM(cache_read_input_tokens), 0) AS SIGNED) AS cache_read_tokens,
CAST(COALESCE(SUM(total_cost_usd), 0.0) AS DOUBLE) AS total_cost,
CAST(COALESCE(SUM(actual_total_cost_usd), 0.0) AS DOUBLE) AS actual_total_cost,
CAST(COALESCE(AVG(response_time_ms), 0.0) AS DOUBLE) AS avg_response_time_ms
FROM `usage`
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#;
async fn perform_mysql_stats_hourly_aggregation(
pool: &MysqlPool,
hour_utc_unix_secs: i64,
aggregated_at: DateTime<Utc>,
) -> Result<StatsHourlyAggregationSummary, DataLayerError> {
let start_unix_secs = hour_utc_unix_secs;
let end_unix_secs = hour_utc_unix_secs + 3600;
let aggregated_at_unix_secs = unix_secs(aggregated_at);
let mut tx = pool.begin().await.map_sql_err()?;
let row = sqlx::query(MYSQL_STATS_AGGREGATE_SQL)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut *tx)
.await
.map_sql_err()?;
let total_requests: i64 = row.try_get("total_requests").map_sql_err()?;
let error_requests: i64 = row.try_get("error_requests").map_sql_err()?;
sqlx::query(
r#"
INSERT INTO stats_hourly (
id, hour_utc, total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, actual_total_cost, avg_response_time_ms, is_complete,
aggregated_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?, ?)
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests),
success_requests = VALUES(success_requests),
error_requests = VALUES(error_requests),
input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens),
cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens),
total_cost = VALUES(total_cost),
actual_total_cost = VALUES(actual_total_cost),
avg_response_time_ms = VALUES(avg_response_time_ms),
is_complete = VALUES(is_complete),
aggregated_at = VALUES(aggregated_at),
updated_at = VALUES(updated_at)
"#,
)
.bind(stats_id(&format!("stats-hourly:{hour_utc_unix_secs}")))
.bind(hour_utc_unix_secs)
.bind(total_requests)
.bind(total_requests.saturating_sub(error_requests))
.bind(error_requests)
.bind(row.try_get::<i64, _>("input_tokens").map_sql_err()?)
.bind(row.try_get::<i64, _>("output_tokens").map_sql_err()?)
.bind(
row.try_get::<i64, _>("cache_creation_tokens")
.map_sql_err()?,
)
.bind(row.try_get::<i64, _>("cache_read_tokens").map_sql_err()?)
.bind(row.try_get::<f64, _>("total_cost").map_sql_err()?)
.bind(row.try_get::<f64, _>("actual_total_cost").map_sql_err()?)
.bind(
row.try_get::<f64, _>("avg_response_time_ms")
.map_sql_err()?,
)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.execute(&mut *tx)
.await
.map_sql_err()?;
let user_rows = upsert_mysql_stats_hourly_user_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let user_model_rows = upsert_mysql_stats_hourly_user_model_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let model_rows = upsert_mysql_stats_hourly_model_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let provider_rows = upsert_mysql_stats_hourly_provider_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
advanced::refresh_hourly(&mut tx, hour_utc_unix_secs, start_unix_secs, end_unix_secs).await?;
tx.commit().await.map_sql_err()?;
Ok(StatsHourlyAggregationSummary {
hour_utc: utc_from_unix_secs(hour_utc_unix_secs, "stats_hourly.hour_utc")?,
total_requests,
user_rows,
user_model_rows,
model_rows,
provider_rows,
})
}
async fn perform_mysql_stats_daily_aggregation(
pool: &MysqlPool,
day_start_unix_secs: i64,
aggregated_at: DateTime<Utc>,
) -> Result<StatsDailyAggregationSummary, DataLayerError> {
let start_unix_secs = day_start_unix_secs;
let end_unix_secs = day_start_unix_secs + 86_400;
let aggregated_at_unix_secs = unix_secs(aggregated_at);
let mut tx = pool.begin().await.map_sql_err()?;
let row = sqlx::query(MYSQL_STATS_AGGREGATE_SQL)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut *tx)
.await
.map_sql_err()?;
let total_requests: i64 = row.try_get("total_requests").map_sql_err()?;
let error_requests: i64 = row.try_get("error_requests").map_sql_err()?;
let unique_models =
mysql_group_count(&mut tx, "model", start_unix_secs, end_unix_secs).await? as i64;
let unique_providers =
mysql_group_count(&mut tx, "provider_name", start_unix_secs, end_unix_secs).await? as i64;
let fallback_count =
mysql_daily_fallback_count(&mut tx, start_unix_secs, end_unix_secs).await?;
sqlx::query(
r#"
INSERT INTO stats_daily (
id, `date`, total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, actual_total_cost, avg_response_time_ms, fallback_count,
unique_models, unique_providers, is_complete, aggregated_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE, ?, ?, ?)
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests),
success_requests = VALUES(success_requests),
error_requests = VALUES(error_requests),
input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens),
cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens),
total_cost = VALUES(total_cost),
actual_total_cost = VALUES(actual_total_cost),
avg_response_time_ms = VALUES(avg_response_time_ms),
fallback_count = VALUES(fallback_count),
unique_models = VALUES(unique_models),
unique_providers = VALUES(unique_providers),
is_complete = VALUES(is_complete),
aggregated_at = VALUES(aggregated_at),
updated_at = VALUES(updated_at)
"#,
)
.bind(stats_id(&format!("stats-daily:{day_start_unix_secs}")))
.bind(day_start_unix_secs)
.bind(total_requests)
.bind(total_requests.saturating_sub(error_requests))
.bind(error_requests)
.bind(row.try_get::<i64, _>("input_tokens").map_sql_err()?)
.bind(row.try_get::<i64, _>("output_tokens").map_sql_err()?)
.bind(
row.try_get::<i64, _>("cache_creation_tokens")
.map_sql_err()?,
)
.bind(row.try_get::<i64, _>("cache_read_tokens").map_sql_err()?)
.bind(row.try_get::<f64, _>("total_cost").map_sql_err()?)
.bind(row.try_get::<f64, _>("actual_total_cost").map_sql_err()?)
.bind(
row.try_get::<f64, _>("avg_response_time_ms")
.map_sql_err()?,
)
.bind(fallback_count)
.bind(unique_models)
.bind(unique_providers)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.execute(&mut *tx)
.await
.map_sql_err()?;
let model_rows = upsert_mysql_stats_daily_model_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let provider_rows = upsert_mysql_stats_daily_provider_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let api_key_rows = upsert_mysql_stats_daily_api_key_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let error_rows = refresh_mysql_stats_daily_error_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let user_rows = upsert_mysql_stats_user_daily_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
advanced::refresh_daily(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
tx.commit().await.map_sql_err()?;
Ok(StatsDailyAggregationSummary {
day_start_utc: utc_from_unix_secs(day_start_unix_secs, "stats_daily.date")?,
total_requests,
model_rows,
provider_rows,
api_key_rows,
error_rows,
user_rows,
})
}
async fn upsert_mysql_stats_hourly_user_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_user (
id, hour_utc, user_id, total_requests, success_requests, error_requests,
input_tokens, output_tokens, total_cost, created_at, updated_at
)
SELECT
SHA2(UUID(), 256), ?, user_id, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN status = 'failed' OR status_code >= 400 OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN status = 'failed' OR status_code >= 400 OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(total_cost_usd), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND user_id IS NOT NULL AND user_id <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY user_id
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), success_requests = VALUES(success_requests),
error_requests = VALUES(error_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), total_cost = VALUES(total_cost),
updated_at = VALUES(updated_at)
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_hourly_user_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_user_model (
id, hour_utc, user_id, model, total_requests, input_tokens, output_tokens,
total_cost, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, user_id, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(total_cost_usd), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND user_id IS NOT NULL AND user_id <> '' AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY user_id, model
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), total_cost = VALUES(total_cost),
updated_at = VALUES(updated_at)
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_hourly_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_model (
id, hour_utc, model, total_requests, input_tokens, output_tokens, total_cost,
avg_response_time_ms, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(total_cost_usd), 0), COALESCE(AVG(response_time_ms), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY model
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), total_cost = VALUES(total_cost),
avg_response_time_ms = VALUES(avg_response_time_ms), updated_at = VALUES(updated_at)
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_hourly_provider_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_provider (
id, hour_utc, provider_name, total_requests, input_tokens, output_tokens,
total_cost, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, provider_name, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(total_cost_usd), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY provider_name
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), total_cost = VALUES(total_cost),
updated_at = VALUES(updated_at)
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_daily_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_model (
id, `date`, model, total_requests, input_tokens, output_tokens,
cache_creation_tokens, cache_read_tokens, total_cost, avg_response_time_ms,
created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(cache_creation_input_tokens), 0),
COALESCE(SUM(cache_read_input_tokens), 0), COALESCE(SUM(total_cost_usd), 0),
COALESCE(AVG(response_time_ms), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY model
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens), total_cost = VALUES(total_cost),
avg_response_time_ms = VALUES(avg_response_time_ms), updated_at = VALUES(updated_at)
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_daily_provider_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_provider (
id, `date`, provider_name, total_requests, input_tokens, output_tokens,
cache_creation_tokens, cache_read_tokens, total_cost, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, provider_name, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(cache_creation_input_tokens), 0),
COALESCE(SUM(cache_read_input_tokens), 0), COALESCE(SUM(total_cost_usd), 0), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY provider_name
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens), total_cost = VALUES(total_cost),
updated_at = VALUES(updated_at)
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_daily_api_key_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_api_key (
id, api_key_id, `date`, total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, api_key_name, created_at, updated_at
)
SELECT SHA2(UUID(), 256), usage.api_key_id, ?, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(usage.input_tokens), 0), COALESCE(SUM(usage.output_tokens), 0),
COALESCE(SUM(usage.cache_creation_input_tokens), 0),
COALESCE(SUM(usage.cache_read_input_tokens), 0),
COALESCE(SUM(usage.total_cost_usd), 0), MAX(api_keys.name), ?, ?
FROM `usage` AS `usage`
LEFT JOIN api_keys ON api_keys.id = usage.api_key_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.api_key_id IS NOT NULL AND usage.api_key_id <> ''
GROUP BY usage.api_key_id
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), success_requests = VALUES(success_requests),
error_requests = VALUES(error_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens), total_cost = VALUES(total_cost),
api_key_name = COALESCE(VALUES(api_key_name), stats_daily_api_key.api_key_name),
updated_at = VALUES(updated_at)
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn refresh_mysql_stats_daily_error_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
sqlx::query("DELETE FROM stats_daily_error WHERE `date` = ?")
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
let result = sqlx::query(
r#"
INSERT INTO stats_daily_error (
id, `date`, error_category, provider_name, model, count, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, error_category, provider_name, model, COUNT(*), ?, ?
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND error_category IS NOT NULL AND error_category <> ''
GROUP BY error_category, provider_name, model
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_mysql_stats_user_daily_rows(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_user_daily (
id, user_id, `date`, total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, username, created_at, updated_at
)
SELECT SHA2(UUID(), 256), usage.user_id, ?, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(usage.input_tokens), 0), COALESCE(SUM(usage.output_tokens), 0),
COALESCE(SUM(usage.cache_creation_input_tokens), 0),
COALESCE(SUM(usage.cache_read_input_tokens), 0),
COALESCE(SUM(usage.total_cost_usd), 0), MAX(users.username), ?, ?
FROM `usage` AS `usage`
LEFT JOIN users ON users.id = usage.user_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND usage.status NOT IN ('pending', 'streaming')
AND usage.provider_name NOT IN ('unknown', 'pending')
GROUP BY usage.user_id
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), success_requests = VALUES(success_requests),
error_requests = VALUES(error_requests), input_tokens = VALUES(input_tokens),
output_tokens = VALUES(output_tokens), cache_creation_tokens = VALUES(cache_creation_tokens),
cache_read_tokens = VALUES(cache_read_tokens), total_cost = VALUES(total_cost),
username = COALESCE(VALUES(username), stats_user_daily.username),
updated_at = VALUES(updated_at)
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn mysql_daily_fallback_count(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<i64, DataLayerError> {
let start_unix_ms = start_unix_secs.checked_mul(1000).ok_or_else(|| {
DataLayerError::InvalidInput("stats fallback window start overflows milliseconds".into())
})?;
let end_unix_ms = end_unix_secs.checked_mul(1000).ok_or_else(|| {
DataLayerError::InvalidInput("stats fallback window end overflows milliseconds".into())
})?;
sqlx::query_scalar(
r#"
SELECT COUNT(*)
FROM (
SELECT request_id
FROM request_candidates
WHERE created_at >= ? AND created_at < ?
AND status IN ('success', 'failed')
GROUP BY request_id
HAVING COUNT(id) > 1
) AS fallback_requests
"#,
)
.bind(start_unix_ms)
.bind(end_unix_ms)
.fetch_one(&mut **tx)
.await
.map_sql_err()
}
async fn mysql_group_count(
tx: &mut sqlx::Transaction<'_, sqlx::MySql>,
group_columns: &str,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let not_empty = group_columns
.split(',')
.map(str::trim)
.map(|column| format!("{column} IS NOT NULL AND {column} <> ''"))
.collect::<Vec<_>>()
.join(" AND ");
let sql = format!(
r#"
SELECT COUNT(*)
FROM (
SELECT 1
FROM `usage`
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
AND {not_empty}
GROUP BY {group_columns}
) AS grouped
"#
);
let count: i64 = sqlx::query_scalar(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(count.max(0)).unwrap_or(usize::MAX))
}
@@ -1,973 +0,0 @@
use sqlx::MySql;
use crate::backend::stats_common::stats_id;
use crate::error::SqlResultExt;
use crate::DataLayerError;
const CACHE_5M: &str = r#"GREATEST(
COALESCE(usage.cache_creation_input_tokens_5m, 0),
COALESCE(usage.cache_creation_ephemeral_5m_input_tokens, 0)
)"#;
const CACHE_1H: &str = r#"GREATEST(
COALESCE(usage.cache_creation_input_tokens_1h, 0),
COALESCE(usage.cache_creation_ephemeral_1h_input_tokens, 0)
)"#;
const CACHE_CREATION: &str = r#"CASE
WHEN COALESCE(usage.cache_creation_input_tokens, 0) = 0
AND ({cache_5m} + {cache_1h}) > 0
THEN {cache_5m} + {cache_1h}
ELSE GREATEST(COALESCE(usage.cache_creation_input_tokens, 0), 0)
END"#;
const EFFECTIVE_INPUT: &str = r#"CASE
WHEN SUBSTRING_INDEX(
LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, '')), ':', 1
) IN ('openai', 'gemini', 'google')
AND COALESCE(usage.input_tokens, 0) > 0
AND COALESCE(usage.cache_read_input_tokens, 0) > 0
THEN GREATEST(COALESCE(usage.input_tokens, 0) - COALESCE(usage.cache_read_input_tokens, 0), 0)
ELSE GREATEST(COALESCE(usage.input_tokens, 0), 0)
END"#;
const SUCCESS: &str = r#"CASE
WHEN usage.status <> 'failed'
AND (usage.status_code IS NULL OR usage.status_code < 400)
AND usage.error_message IS NULL
THEN 1 ELSE 0
END"#;
const AGGREGATABLE: &str = r#"usage.status NOT IN ('pending', 'streaming')
AND usage.provider_name NOT IN ('unknown', 'pending')"#;
const SETTLED: &str = r#"COALESCE(settlement.billing_status, usage.billing_status) = 'settled'
AND COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) > 0"#;
fn cache_creation_expr() -> String {
CACHE_CREATION
.replace("{cache_5m}", CACHE_5M)
.replace("{cache_1h}", CACHE_1H)
}
fn total_input_context_expr() -> String {
format!(
"({EFFECTIVE_INPUT}) + ({}) + GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)",
cache_creation_expr()
)
}
fn total_tokens_expr() -> String {
format!(
r#"COALESCE(
NULLIF(GREATEST(COALESCE(usage.total_tokens, 0), 0), 0),
({EFFECTIVE_INPUT})
+ GREATEST(COALESCE(usage.output_tokens, 0), 0)
+ ({})
+ GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0),
0
)"#,
cache_creation_expr()
)
}
fn percentile_cont(sorted: &[i64], percentile: f64) -> Option<i64> {
if sorted.is_empty() {
return None;
}
let position = percentile * (sorted.len().saturating_sub(1) as f64);
let lower = position.floor() as usize;
let upper = position.ceil() as usize;
let fraction = position - lower as f64;
let value = sorted[lower] as f64 + (sorted[upper] - sorted[lower]) as f64 * fraction;
Some(value.round() as i64)
}
async fn load_percentiles(
tx: &mut sqlx::Transaction<'_, MySql>,
column: &str,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(Option<i64>, Option<i64>, Option<i64>), DataLayerError> {
let sql = format!(
r#"
SELECT {column}
FROM `usage`
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status = 'completed'
AND provider_name NOT IN ('unknown', 'pending')
AND {column} IS NOT NULL
ORDER BY {column}
"#
);
let values: Vec<i64> = sqlx::query_scalar(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_all(&mut **tx)
.await
.map_sql_err()?;
if values.len() < 10 {
return Ok((None, None, None));
}
Ok((
percentile_cont(&values, 0.50),
percentile_cont(&values, 0.90),
percentile_cont(&values, 0.99),
))
}
pub(super) async fn refresh_hourly(
tx: &mut sqlx::Transaction<'_, MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let sql = format!(
r#"
UPDATE stats_hourly AS target
JOIN (
SELECT
COUNT(*) AS cache_hit_total_requests,
COALESCE(SUM(CASE WHEN COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN 1 ELSE 0 END), 0) AS completed_total_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' AND COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS completed_cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS completed_input_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({cache_creation}) ELSE 0 END), 0) AS completed_cache_creation_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS completed_cache_read_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({total_context}) ELSE 0 END), 0) AS completed_total_input_context,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_creation_cost,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_read_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN 1 ELSE 0 END), 0) AS response_time_samples
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
) AS aggregated
SET
target.cache_hit_total_requests = aggregated.cache_hit_total_requests,
target.cache_hit_requests = aggregated.cache_hit_requests,
target.completed_total_requests = aggregated.completed_total_requests,
target.completed_cache_hit_requests = aggregated.completed_cache_hit_requests,
target.completed_input_tokens = aggregated.completed_input_tokens,
target.completed_cache_creation_tokens = aggregated.completed_cache_creation_tokens,
target.completed_cache_read_tokens = aggregated.completed_cache_read_tokens,
target.completed_total_input_context = aggregated.completed_total_input_context,
target.completed_cache_creation_cost = aggregated.completed_cache_creation_cost,
target.completed_cache_read_cost = aggregated.completed_cache_read_cost,
target.settled_total_cost = aggregated.settled_total_cost,
target.settled_total_requests = aggregated.settled_total_requests,
target.settled_input_tokens = aggregated.settled_input_tokens,
target.settled_output_tokens = aggregated.settled_output_tokens,
target.settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
target.settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
target.settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
target.settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs,
target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples
WHERE target.hour_utc = ?
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
refresh_hourly_user(tx, hour_utc, start_unix_secs, end_unix_secs).await?;
refresh_hourly_response_dimensions(tx, hour_utc, start_unix_secs, end_unix_secs).await
}
async fn refresh_hourly_user(
tx: &mut sqlx::Transaction<'_, MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let sql = format!(
r#"
UPDATE stats_hourly_user AS target
JOIN (
SELECT usage.user_id,
COALESCE(SUM({cache_creation}), 0) AS cache_creation_tokens,
COALESCE(SUM(GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens,
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0) AS actual_total_cost,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id
) AS aggregated ON target.user_id = aggregated.user_id
SET target.cache_creation_tokens = aggregated.cache_creation_tokens,
target.cache_read_tokens = aggregated.cache_read_tokens,
target.actual_total_cost = aggregated.actual_total_cost,
target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples,
target.settled_total_cost = aggregated.settled_total_cost,
target.settled_total_requests = aggregated.settled_total_requests,
target.settled_input_tokens = aggregated.settled_input_tokens,
target.settled_output_tokens = aggregated.settled_output_tokens,
target.settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
target.settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
target.settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
target.settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs
WHERE target.hour_utc = ?
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_hourly_response_dimensions(
tx: &mut sqlx::Transaction<'_, MySql>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
for (table, select_dimensions, group_by, join) in [
(
"stats_hourly_model",
"usage.model AS model",
"usage.model",
"target.model = aggregated.model",
),
(
"stats_hourly_user_model",
"usage.user_id AS user_id, usage.model AS model",
"usage.user_id, usage.model",
"target.user_id = aggregated.user_id AND target.model = aggregated.model",
),
] {
let sql = format!(
r#"
UPDATE {table} AS target
JOIN (
SELECT {select_dimensions},
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples
FROM `usage` AS `usage`
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ? AND {AGGREGATABLE}
GROUP BY {group_by}
) AS aggregated ON {join}
SET target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples
WHERE target.hour_utc = ?
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
}
Ok(())
}
pub(super) async fn refresh_daily(
tx: &mut sqlx::Transaction<'_, MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let response = load_percentiles(tx, "response_time_ms", start_unix_secs, end_unix_secs).await?;
let first_byte =
load_percentiles(tx, "first_byte_time_ms", start_unix_secs, end_unix_secs).await?;
refresh_daily_root(
tx,
day_start,
start_unix_secs,
end_unix_secs,
response,
first_byte,
)
.await?;
refresh_daily_existing_dimensions(tx, day_start, start_unix_secs, end_unix_secs).await?;
upsert_user_dimension(
tx,
"stats_user_daily_model",
"model",
"usage.model",
"usage.model IS NOT NULL AND usage.model <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_user_dimension(
tx,
"stats_user_daily_provider",
"provider_name",
"usage.provider_name",
"usage.provider_name IS NOT NULL AND usage.provider_name <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_user_dimension(
tx,
"stats_user_daily_api_format",
"api_format",
"LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, ''))",
"COALESCE(usage.endpoint_api_format, usage.api_format, '') <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_model_provider_rows(tx, day_start, start_unix_secs, end_unix_secs, now_unix_secs)
.await?;
upsert_cost_savings_rows(tx, day_start, start_unix_secs, end_unix_secs, now_unix_secs).await?;
refresh_user_summary(tx, end_unix_secs, now_unix_secs).await
}
async fn refresh_daily_root(
tx: &mut sqlx::Transaction<'_, MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
response: (Option<i64>, Option<i64>, Option<i64>),
first_byte: (Option<i64>, Option<i64>, Option<i64>),
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let sql = format!(
r#"
UPDATE stats_daily AS target
JOIN (
SELECT
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN ({EFFECTIVE_INPUT}) ELSE 0 END), 0) AS effective_input_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN ({total_context}) ELSE 0 END), 0) AS total_input_context,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN {CACHE_5M} ELSE 0 END), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN {CACHE_1H} ELSE 0 END), 0) AS cache_creation_ephemeral_1h_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.input_cost_usd, 0) ELSE 0 END), 0) AS input_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.output_cost_usd, 0) ELSE 0 END), 0) AS output_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS cache_creation_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS cache_read_cost,
COUNT(*) AS cache_hit_total_requests,
COALESCE(SUM(CASE WHEN COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN 1 ELSE 0 END), 0) AS completed_total_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' AND COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS completed_cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS completed_input_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({cache_creation}) ELSE 0 END), 0) AS completed_cache_creation_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS completed_cache_read_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({total_context}) ELSE 0 END), 0) AS completed_total_input_context,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_creation_cost,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_read_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
) AS aggregated
SET target.effective_input_tokens = aggregated.effective_input_tokens,
target.total_input_context = aggregated.total_input_context,
target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples,
target.cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
target.cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens,
target.input_cost = aggregated.input_cost,
target.output_cost = aggregated.output_cost,
target.cache_creation_cost = aggregated.cache_creation_cost,
target.cache_read_cost = aggregated.cache_read_cost,
target.cache_hit_total_requests = aggregated.cache_hit_total_requests,
target.cache_hit_requests = aggregated.cache_hit_requests,
target.completed_total_requests = aggregated.completed_total_requests,
target.completed_cache_hit_requests = aggregated.completed_cache_hit_requests,
target.completed_input_tokens = aggregated.completed_input_tokens,
target.completed_cache_creation_tokens = aggregated.completed_cache_creation_tokens,
target.completed_cache_read_tokens = aggregated.completed_cache_read_tokens,
target.completed_total_input_context = aggregated.completed_total_input_context,
target.completed_cache_creation_cost = aggregated.completed_cache_creation_cost,
target.completed_cache_read_cost = aggregated.completed_cache_read_cost,
target.settled_total_cost = aggregated.settled_total_cost,
target.settled_total_requests = aggregated.settled_total_requests,
target.settled_input_tokens = aggregated.settled_input_tokens,
target.settled_output_tokens = aggregated.settled_output_tokens,
target.settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
target.settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
target.settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
target.settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs,
target.p50_response_time_ms = ?, target.p90_response_time_ms = ?, target.p99_response_time_ms = ?,
target.p50_first_byte_time_ms = ?, target.p90_first_byte_time_ms = ?, target.p99_first_byte_time_ms = ?
WHERE target.`date` = ?
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(response.0)
.bind(response.1)
.bind(response.2)
.bind(first_byte.0)
.bind(first_byte.1)
.bind(first_byte.2)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_daily_existing_dimensions(
tx: &mut sqlx::Transaction<'_, MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let model_sql = format!(
r#"
UPDATE stats_daily_model AS target
JOIN (
SELECT usage.model,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM({CACHE_5M}), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM({CACHE_1H}), 0) AS cache_creation_ephemeral_1h_tokens
FROM `usage` AS `usage`
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND {AGGREGATABLE} AND usage.model IS NOT NULL AND usage.model <> ''
GROUP BY usage.model
) AS aggregated ON target.model = aggregated.model
SET target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples,
target.cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
target.cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens
WHERE target.`date` = ?
"#
);
sqlx::query(&model_sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
let user_sql = format!(
r#"
UPDATE stats_user_daily AS target
JOIN (
SELECT usage.user_id,
COALESCE(SUM({EFFECTIVE_INPUT}), 0) AS effective_input_tokens,
COALESCE(SUM({total_context}), 0) AS total_input_context,
COALESCE(SUM(COALESCE(usage.cache_creation_cost_usd, 0)), 0) AS cache_creation_cost,
COALESCE(SUM(COALESCE(usage.cache_read_cost_usd, 0)), 0) AS cache_read_cost,
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0) AS actual_total_cost,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM({CACHE_5M}), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM({CACHE_1H}), 0) AS cache_creation_ephemeral_1h_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id
) AS aggregated ON target.user_id = aggregated.user_id
SET target.effective_input_tokens = aggregated.effective_input_tokens,
target.total_input_context = aggregated.total_input_context,
target.cache_creation_cost = aggregated.cache_creation_cost,
target.cache_read_cost = aggregated.cache_read_cost,
target.actual_total_cost = aggregated.actual_total_cost,
target.response_time_sum_ms = aggregated.response_time_sum_ms,
target.response_time_samples = aggregated.response_time_samples,
target.cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
target.cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens,
target.settled_total_cost = aggregated.settled_total_cost,
target.settled_total_requests = aggregated.settled_total_requests,
target.settled_input_tokens = aggregated.settled_input_tokens,
target.settled_output_tokens = aggregated.settled_output_tokens,
target.settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
target.settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
target.settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
target.settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs
WHERE target.`date` = ?
"#
);
sqlx::query(&user_sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn upsert_user_dimension(
tx: &mut sqlx::Transaction<'_, MySql>,
table: &str,
dimension_column: &str,
dimension_expr: &str,
dimension_filter: &str,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let total_tokens = total_tokens_expr();
let sql = format!(
r#"
INSERT INTO {table} (
id, user_id, username, `date`, {dimension_column}, total_requests, success_requests,
input_tokens, effective_input_tokens, output_tokens, total_tokens, total_input_context,
cache_creation_tokens, cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens, cache_read_tokens, total_cost, actual_total_cost,
response_time_sum_ms, response_time_samples, successful_response_time_sum_ms,
successful_response_time_samples, created_at, updated_at
)
SELECT SHA2(UUID(), 256), usage.user_id,
MAX(COALESCE(usage.username, users.username)), ?, {dimension_expr}, COUNT(*),
COALESCE(SUM({SUCCESS}), 0),
COALESCE(SUM(GREATEST(COALESCE(usage.input_tokens, 0), 0)), 0),
COALESCE(SUM({EFFECTIVE_INPUT}), 0),
COALESCE(SUM(GREATEST(COALESCE(usage.output_tokens, 0), 0)), 0),
COALESCE(SUM({total_tokens}), 0), COALESCE(SUM({total_context}), 0),
COALESCE(SUM({cache_creation}), 0), COALESCE(SUM({CACHE_5M}), 0),
COALESCE(SUM({CACHE_1H}), 0),
COALESCE(SUM(GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN ({SUCCESS}) = 1 AND usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN ({SUCCESS}) = 1 AND usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0),
?, ?
FROM `usage` AS `usage`
LEFT JOIN users ON users.id = usage.user_id
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND {dimension_filter} AND {AGGREGATABLE}
GROUP BY usage.user_id, {dimension_expr}
ON DUPLICATE KEY UPDATE
username = COALESCE(VALUES(username), {table}.username),
total_requests = VALUES(total_requests), success_requests = VALUES(success_requests),
input_tokens = VALUES(input_tokens), effective_input_tokens = VALUES(effective_input_tokens),
output_tokens = VALUES(output_tokens), total_tokens = VALUES(total_tokens),
total_input_context = VALUES(total_input_context),
cache_creation_tokens = VALUES(cache_creation_tokens),
cache_creation_ephemeral_5m_tokens = VALUES(cache_creation_ephemeral_5m_tokens),
cache_creation_ephemeral_1h_tokens = VALUES(cache_creation_ephemeral_1h_tokens),
cache_read_tokens = VALUES(cache_read_tokens), total_cost = VALUES(total_cost),
actual_total_cost = VALUES(actual_total_cost),
response_time_sum_ms = VALUES(response_time_sum_ms),
response_time_samples = VALUES(response_time_samples),
successful_response_time_sum_ms = VALUES(successful_response_time_sum_ms),
successful_response_time_samples = VALUES(successful_response_time_samples),
updated_at = VALUES(updated_at)
"#
);
sqlx::query(&sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn upsert_model_provider_rows(
tx: &mut sqlx::Transaction<'_, MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let total_tokens = total_tokens_expr();
let model_provider_sql = format!(
r#"
INSERT INTO stats_daily_model_provider (
id, `date`, model, provider_name, total_requests, total_tokens, total_cost,
response_time_sum_ms, response_time_samples, created_at, updated_at
)
SELECT SHA2(UUID(), 256), ?, usage.model, usage.provider_name, COUNT(*),
COALESCE(SUM({total_tokens}), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0), ?, ?
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.model IS NOT NULL AND usage.model <> '' AND {AGGREGATABLE}
GROUP BY usage.model, usage.provider_name
ON DUPLICATE KEY UPDATE
total_requests = VALUES(total_requests), total_tokens = VALUES(total_tokens),
total_cost = VALUES(total_cost), response_time_sum_ms = VALUES(response_time_sum_ms),
response_time_samples = VALUES(response_time_samples), updated_at = VALUES(updated_at)
"#
);
sqlx::query(&model_provider_sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
let user_model_provider_sql = format!(
r#"
INSERT INTO stats_user_daily_model_provider (
id, user_id, username, `date`, model, provider_name, total_requests, total_tokens,
total_cost, response_time_sum_ms, response_time_samples, created_at, updated_at
)
SELECT SHA2(UUID(), 256), usage.user_id, MAX(COALESCE(usage.username, users.username)),
?, usage.model, usage.provider_name, COUNT(*), COALESCE(SUM({total_tokens}), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN GREATEST(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0), ?, ?
FROM `usage` AS `usage`
LEFT JOIN users ON users.id = usage.user_id
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND usage.model IS NOT NULL AND usage.model <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id, usage.model, usage.provider_name
ON DUPLICATE KEY UPDATE
username = COALESCE(VALUES(username), stats_user_daily_model_provider.username),
total_requests = VALUES(total_requests), total_tokens = VALUES(total_tokens),
total_cost = VALUES(total_cost), response_time_sum_ms = VALUES(response_time_sum_ms),
response_time_samples = VALUES(response_time_samples), updated_at = VALUES(updated_at)
"#
);
sqlx::query(&user_model_provider_sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn upsert_cost_savings_rows(
tx: &mut sqlx::Transaction<'_, MySql>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
for (table, dimensions) in [
("stats_daily_cost_savings", Vec::new()),
(
"stats_daily_cost_savings_provider",
vec![("provider_name", "COALESCE(usage.provider_name, '')")],
),
(
"stats_daily_cost_savings_model",
vec![("model", "COALESCE(usage.model, '')")],
),
(
"stats_daily_cost_savings_model_provider",
vec![
("model", "COALESCE(usage.model, '')"),
("provider_name", "COALESCE(usage.provider_name, '')"),
],
),
] {
upsert_cost_savings_dimension(
tx,
table,
false,
&dimensions,
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
}
for (table, dimensions) in [
("stats_user_daily_cost_savings", Vec::new()),
(
"stats_user_daily_cost_savings_provider",
vec![("provider_name", "COALESCE(usage.provider_name, '')")],
),
(
"stats_user_daily_cost_savings_model",
vec![("model", "COALESCE(usage.model, '')")],
),
(
"stats_user_daily_cost_savings_model_provider",
vec![
("model", "COALESCE(usage.model, '')"),
("provider_name", "COALESCE(usage.provider_name, '')"),
],
),
] {
upsert_cost_savings_dimension(
tx,
table,
true,
&dimensions,
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn upsert_cost_savings_dimension(
tx: &mut sqlx::Transaction<'_, MySql>,
table: &str,
per_user: bool,
dimensions: &[(&str, &str)],
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let dimension_columns = dimensions
.iter()
.map(|(column, _)| *column)
.collect::<Vec<_>>();
let dimension_exprs = dimensions
.iter()
.map(|(_, expression)| *expression)
.collect::<Vec<_>>();
let user_columns = if per_user { "user_id, username, " } else { "" };
let user_select = if per_user {
"usage.user_id, MAX(COALESCE(usage.username, users.username)), "
} else {
""
};
let user_join = if per_user {
"LEFT JOIN users ON users.id = usage.user_id"
} else {
""
};
let user_filter = if per_user {
"AND usage.user_id IS NOT NULL AND usage.user_id <> ''"
} else {
""
};
let mut group_by = Vec::new();
if per_user {
group_by.push("usage.user_id");
}
group_by.extend(dimension_exprs.iter().copied());
let dimension_columns_sql = if dimension_columns.is_empty() {
String::new()
} else {
format!("{}, ", dimension_columns.join(", "))
};
let dimension_select_sql = if dimension_exprs.is_empty() {
String::new()
} else {
format!("{}, ", dimension_exprs.join(", "))
};
let group_by_sql = if group_by.is_empty() {
String::new()
} else {
format!("GROUP BY {}", group_by.join(", "))
};
let sql = format!(
r#"
INSERT INTO {table} (
id, {user_columns}`date`, {dimension_columns_sql}cache_read_tokens,
cache_read_cost, cache_creation_cost, estimated_full_cost, created_at, updated_at
)
SELECT SHA2(UUID(), 256), {user_select}?, {dimension_select_sql}
COALESCE(SUM(GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0),
COALESCE(SUM(COALESCE(usage.cache_read_cost_usd, 0)), 0),
COALESCE(SUM(COALESCE(usage.cache_creation_cost_usd, 0)), 0),
COALESCE(SUM(
COALESCE(settlement.input_price_per_1m, usage.input_price_per_1m, 0)
* GREATEST(COALESCE(usage.cache_read_input_tokens, 0), 0) / 1000000.0
), 0), ?, ?
FROM `usage` AS `usage`
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
{user_join}
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ? {user_filter}
{group_by_sql}
ON DUPLICATE KEY UPDATE
{}cache_read_tokens = VALUES(cache_read_tokens),
cache_read_cost = VALUES(cache_read_cost),
cache_creation_cost = VALUES(cache_creation_cost),
estimated_full_cost = VALUES(estimated_full_cost), updated_at = VALUES(updated_at)
"#,
if per_user {
format!("username = COALESCE(VALUES(username), {table}.username), ")
} else {
String::new()
}
);
sqlx::query(&sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_user_summary(
tx: &mut sqlx::Transaction<'_, MySql>,
cutoff_date: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
sqlx::query(
r#"
INSERT INTO stats_user_summary (
id, user_id, username, cutoff_date, all_time_requests, all_time_success_requests,
all_time_error_requests, all_time_input_tokens, all_time_output_tokens,
all_time_cache_creation_tokens, all_time_cache_read_tokens, all_time_cost,
all_time_actual_cost, active_days, first_active_date, last_active_date,
created_at, updated_at
)
SELECT SHA2(UUID(), 256), user_id, MAX(username), ?,
COALESCE(SUM(total_requests), 0), COALESCE(SUM(success_requests), 0),
COALESCE(SUM(error_requests), 0), COALESCE(SUM(input_tokens), 0),
COALESCE(SUM(output_tokens), 0), COALESCE(SUM(cache_creation_tokens), 0),
COALESCE(SUM(cache_read_tokens), 0), COALESCE(SUM(total_cost), 0),
COALESCE(SUM(actual_total_cost), 0),
COALESCE(SUM(CASE WHEN total_requests > 0 THEN 1 ELSE 0 END), 0),
MIN(CASE WHEN total_requests > 0 THEN `date` END),
MAX(CASE WHEN total_requests > 0 THEN `date` END), ?, ?
FROM stats_user_daily
WHERE `date` < ?
GROUP BY user_id
ON DUPLICATE KEY UPDATE
username = COALESCE(VALUES(username), stats_user_summary.username),
cutoff_date = VALUES(cutoff_date), all_time_requests = VALUES(all_time_requests),
all_time_success_requests = VALUES(all_time_success_requests),
all_time_error_requests = VALUES(all_time_error_requests),
all_time_input_tokens = VALUES(all_time_input_tokens),
all_time_output_tokens = VALUES(all_time_output_tokens),
all_time_cache_creation_tokens = VALUES(all_time_cache_creation_tokens),
all_time_cache_read_tokens = VALUES(all_time_cache_read_tokens),
all_time_cost = VALUES(all_time_cost), all_time_actual_cost = VALUES(all_time_actual_cost),
active_days = VALUES(active_days), first_active_date = VALUES(first_active_date),
last_active_date = VALUES(last_active_date), updated_at = VALUES(updated_at)
"#,
)
.bind(cutoff_date)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(cutoff_date)
.execute(&mut **tx)
.await
.map_sql_err()?;
refresh_global_summary(tx, cutoff_date, now_unix_secs).await?;
Ok(())
}
async fn refresh_global_summary(
tx: &mut sqlx::Transaction<'_, MySql>,
cutoff_date: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let existing_id: Option<String> =
sqlx::query_scalar("SELECT id FROM stats_summary ORDER BY created_at, id LIMIT 1")
.fetch_optional(&mut **tx)
.await
.map_sql_err()?;
let summary_id = existing_id.unwrap_or_else(|| stats_id("stats-summary"));
sqlx::query(
r#"
INSERT INTO stats_summary (
id, cutoff_date, all_time_requests, all_time_success_requests,
all_time_error_requests, all_time_input_tokens, all_time_output_tokens,
all_time_cache_creation_tokens, all_time_cache_read_tokens, all_time_cost,
all_time_actual_cost, total_users, active_users, total_api_keys,
active_api_keys, created_at, updated_at
)
SELECT ?, ?, COALESCE(SUM(total_requests), 0), COALESCE(SUM(success_requests), 0),
COALESCE(SUM(error_requests), 0), COALESCE(SUM(input_tokens), 0),
COALESCE(SUM(output_tokens), 0), COALESCE(SUM(cache_creation_tokens), 0),
COALESCE(SUM(cache_read_tokens), 0), COALESCE(SUM(total_cost), 0),
COALESCE(SUM(actual_total_cost), 0),
(SELECT COUNT(*) FROM users),
(SELECT COUNT(*) FROM users WHERE is_active <> 0),
(SELECT COUNT(*) FROM api_keys),
(SELECT COUNT(*) FROM api_keys WHERE is_active <> 0), ?, ?
FROM stats_daily
WHERE `date` < ?
ON DUPLICATE KEY UPDATE
cutoff_date = VALUES(cutoff_date),
all_time_requests = VALUES(all_time_requests),
all_time_success_requests = VALUES(all_time_success_requests),
all_time_error_requests = VALUES(all_time_error_requests),
all_time_input_tokens = VALUES(all_time_input_tokens),
all_time_output_tokens = VALUES(all_time_output_tokens),
all_time_cache_creation_tokens = VALUES(all_time_cache_creation_tokens),
all_time_cache_read_tokens = VALUES(all_time_cache_read_tokens),
all_time_cost = VALUES(all_time_cost),
all_time_actual_cost = VALUES(all_time_actual_cost),
total_users = VALUES(total_users), active_users = VALUES(active_users),
total_api_keys = VALUES(total_api_keys), active_api_keys = VALUES(active_api_keys),
updated_at = VALUES(updated_at)
"#,
)
.bind(summary_id)
.bind(cutoff_date)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(cutoff_date)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
@@ -1,871 +0,0 @@
use chrono::{DateTime, Utc};
use sqlx::Row;
use crate::backend::stats_common::{stats_id, unix_secs, utc_from_unix_secs};
use crate::backend::SqliteBackend;
use crate::driver::sqlite::{sqlite_real, SqlitePool};
use crate::error::SqlResultExt;
use crate::{
DataLayerError, StatsDailyAggregationInput, StatsDailyAggregationSummary,
StatsHourlyAggregationInput, StatsHourlyAggregationSummary,
};
mod advanced;
impl SqliteBackend {
pub async fn aggregate_stats_hourly(
&self,
input: &StatsHourlyAggregationInput,
) -> Result<Option<StatsHourlyAggregationSummary>, DataLayerError> {
let Some(hour_utc_unix_secs) =
next_sqlite_stats_hourly_bucket(self.pool(), input.target_hour_utc).await?
else {
return Ok(None);
};
perform_sqlite_stats_hourly_aggregation(
self.pool(),
hour_utc_unix_secs,
input.aggregated_at,
)
.await
.map(Some)
}
pub async fn aggregate_stats_daily(
&self,
input: &StatsDailyAggregationInput,
) -> Result<Option<StatsDailyAggregationSummary>, DataLayerError> {
let Some(day_start_unix_secs) =
next_sqlite_stats_daily_bucket(self.pool(), input.target_day_utc).await?
else {
return Ok(None);
};
perform_sqlite_stats_daily_aggregation(
self.pool(),
day_start_unix_secs,
input.aggregated_at,
)
.await
.map(Some)
}
}
async fn next_sqlite_stats_hourly_bucket(
pool: &SqlitePool,
target_hour_utc: DateTime<Utc>,
) -> Result<Option<i64>, DataLayerError> {
let latest_hour: Option<i64> =
sqlx::query_scalar("SELECT MAX(hour_utc) FROM stats_hourly WHERE is_complete <> 0")
.fetch_one(pool)
.await
.map_sql_err()?;
let search_from = latest_hour.map(|value| value + 3600).unwrap_or(0);
let search_until = unix_secs(target_hour_utc) + 3600;
if search_from >= search_until {
return Ok(None);
}
let next_bucket: Option<i64> = sqlx::query_scalar(
r#"
SELECT MIN(CAST(created_at_unix_ms / 3600 AS INTEGER) * 3600)
FROM "usage"
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#,
)
.bind(search_from)
.bind(search_until)
.fetch_one(pool)
.await
.map_sql_err()?;
Ok(next_bucket.filter(|value| *value <= unix_secs(target_hour_utc)))
}
async fn next_sqlite_stats_daily_bucket(
pool: &SqlitePool,
target_day_utc: DateTime<Utc>,
) -> Result<Option<i64>, DataLayerError> {
let latest_day: Option<i64> =
sqlx::query_scalar(r#"SELECT MAX("date") FROM stats_daily WHERE is_complete <> 0"#)
.fetch_one(pool)
.await
.map_sql_err()?;
let search_from = latest_day.map(|value| value + 86_400).unwrap_or(0);
let search_until = unix_secs(target_day_utc) + 86_400;
if search_from >= search_until {
return Ok(None);
}
let next_bucket: Option<i64> = sqlx::query_scalar(
r#"
SELECT MIN(CAST(created_at_unix_ms / 86400 AS INTEGER) * 86400)
FROM "usage"
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#,
)
.bind(search_from)
.bind(search_until)
.fetch_one(pool)
.await
.map_sql_err()?;
Ok(next_bucket.filter(|value| *value <= unix_secs(target_day_utc)))
}
const SQLITE_STATS_AGGREGATE_SQL: &str = r#"
SELECT
COUNT(*) AS total_requests,
COALESCE(SUM(CASE
WHEN status = 'failed'
OR status_code >= 400
OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0) AS error_requests,
COALESCE(SUM(input_tokens), 0) AS input_tokens,
COALESCE(SUM(output_tokens), 0) AS output_tokens,
COALESCE(SUM(cache_creation_input_tokens), 0) AS cache_creation_tokens,
COALESCE(SUM(cache_read_input_tokens), 0) AS cache_read_tokens,
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL) AS total_cost,
CAST(COALESCE(SUM(actual_total_cost_usd), 0) AS REAL) AS actual_total_cost,
CAST(COALESCE(AVG(response_time_ms), 0) AS REAL) AS avg_response_time_ms
FROM "usage"
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
"#;
async fn perform_sqlite_stats_hourly_aggregation(
pool: &SqlitePool,
hour_utc_unix_secs: i64,
aggregated_at: DateTime<Utc>,
) -> Result<StatsHourlyAggregationSummary, DataLayerError> {
let start_unix_secs = hour_utc_unix_secs;
let end_unix_secs = hour_utc_unix_secs + 3600;
let aggregated_at_unix_secs = unix_secs(aggregated_at);
let mut tx = pool.begin().await.map_sql_err()?;
let row = sqlx::query(SQLITE_STATS_AGGREGATE_SQL)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut *tx)
.await
.map_sql_err()?;
let total_requests: i64 = row.try_get("total_requests").map_sql_err()?;
let error_requests: i64 = row.try_get("error_requests").map_sql_err()?;
sqlx::query(
r#"
INSERT INTO stats_hourly (
id, hour_utc, total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, actual_total_cost, avg_response_time_ms, is_complete,
aggregated_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
ON CONFLICT (hour_utc) DO UPDATE SET
total_requests = excluded.total_requests,
success_requests = excluded.success_requests,
error_requests = excluded.error_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
actual_total_cost = excluded.actual_total_cost,
avg_response_time_ms = excluded.avg_response_time_ms,
is_complete = excluded.is_complete,
aggregated_at = excluded.aggregated_at,
updated_at = excluded.updated_at
"#,
)
.bind(stats_id(&format!("stats-hourly:{hour_utc_unix_secs}")))
.bind(hour_utc_unix_secs)
.bind(total_requests)
.bind(total_requests.saturating_sub(error_requests))
.bind(error_requests)
.bind(row.try_get::<i64, _>("input_tokens").map_sql_err()?)
.bind(row.try_get::<i64, _>("output_tokens").map_sql_err()?)
.bind(
row.try_get::<i64, _>("cache_creation_tokens")
.map_sql_err()?,
)
.bind(row.try_get::<i64, _>("cache_read_tokens").map_sql_err()?)
.bind(sqlite_real(&row, "total_cost")?)
.bind(sqlite_real(&row, "actual_total_cost")?)
.bind(sqlite_real(&row, "avg_response_time_ms")?)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.execute(&mut *tx)
.await
.map_sql_err()?;
let user_rows = upsert_sqlite_stats_hourly_user_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let user_model_rows = upsert_sqlite_stats_hourly_user_model_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let model_rows = upsert_sqlite_stats_hourly_model_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let provider_rows = upsert_sqlite_stats_hourly_provider_rows(
&mut tx,
hour_utc_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
advanced::refresh_hourly(&mut tx, hour_utc_unix_secs, start_unix_secs, end_unix_secs).await?;
tx.commit().await.map_sql_err()?;
Ok(StatsHourlyAggregationSummary {
hour_utc: utc_from_unix_secs(hour_utc_unix_secs, "stats_hourly.hour_utc")?,
total_requests,
user_rows,
user_model_rows,
model_rows,
provider_rows,
})
}
async fn perform_sqlite_stats_daily_aggregation(
pool: &SqlitePool,
day_start_unix_secs: i64,
aggregated_at: DateTime<Utc>,
) -> Result<StatsDailyAggregationSummary, DataLayerError> {
let start_unix_secs = day_start_unix_secs;
let end_unix_secs = day_start_unix_secs + 86_400;
let aggregated_at_unix_secs = unix_secs(aggregated_at);
let mut tx = pool.begin().await.map_sql_err()?;
let row = sqlx::query(SQLITE_STATS_AGGREGATE_SQL)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut *tx)
.await
.map_sql_err()?;
let total_requests: i64 = row.try_get("total_requests").map_sql_err()?;
let error_requests: i64 = row.try_get("error_requests").map_sql_err()?;
let unique_models =
sqlite_group_count(&mut tx, "model", start_unix_secs, end_unix_secs).await? as i64;
let unique_providers =
sqlite_group_count(&mut tx, "provider_name", start_unix_secs, end_unix_secs).await? as i64;
let fallback_count =
sqlite_daily_fallback_count(&mut tx, start_unix_secs, end_unix_secs).await?;
sqlx::query(
r#"
INSERT INTO stats_daily (
id, "date", total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, actual_total_cost, avg_response_time_ms, fallback_count,
unique_models, unique_providers, is_complete, aggregated_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
ON CONFLICT ("date") DO UPDATE SET
total_requests = excluded.total_requests,
success_requests = excluded.success_requests,
error_requests = excluded.error_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
actual_total_cost = excluded.actual_total_cost,
avg_response_time_ms = excluded.avg_response_time_ms,
fallback_count = excluded.fallback_count,
unique_models = excluded.unique_models,
unique_providers = excluded.unique_providers,
is_complete = excluded.is_complete,
aggregated_at = excluded.aggregated_at,
updated_at = excluded.updated_at
"#,
)
.bind(stats_id(&format!("stats-daily:{day_start_unix_secs}")))
.bind(day_start_unix_secs)
.bind(total_requests)
.bind(total_requests.saturating_sub(error_requests))
.bind(error_requests)
.bind(row.try_get::<i64, _>("input_tokens").map_sql_err()?)
.bind(row.try_get::<i64, _>("output_tokens").map_sql_err()?)
.bind(
row.try_get::<i64, _>("cache_creation_tokens")
.map_sql_err()?,
)
.bind(row.try_get::<i64, _>("cache_read_tokens").map_sql_err()?)
.bind(sqlite_real(&row, "total_cost")?)
.bind(sqlite_real(&row, "actual_total_cost")?)
.bind(sqlite_real(&row, "avg_response_time_ms")?)
.bind(fallback_count)
.bind(unique_models)
.bind(unique_providers)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.bind(aggregated_at_unix_secs)
.execute(&mut *tx)
.await
.map_sql_err()?;
let model_rows = upsert_sqlite_stats_daily_model_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let provider_rows = upsert_sqlite_stats_daily_provider_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let api_key_rows = upsert_sqlite_stats_daily_api_key_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let error_rows = refresh_sqlite_stats_daily_error_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
let user_rows = upsert_sqlite_stats_user_daily_rows(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
advanced::refresh_daily(
&mut tx,
day_start_unix_secs,
start_unix_secs,
end_unix_secs,
aggregated_at_unix_secs,
)
.await?;
tx.commit().await.map_sql_err()?;
Ok(StatsDailyAggregationSummary {
day_start_utc: utc_from_unix_secs(day_start_unix_secs, "stats_daily.date")?,
total_requests,
model_rows,
provider_rows,
api_key_rows,
error_rows,
user_rows,
})
}
async fn upsert_sqlite_stats_hourly_user_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_user (
id, hour_utc, user_id, total_requests, success_requests, error_requests,
input_tokens, output_tokens, total_cost, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, user_id, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN status = 'failed' OR status_code >= 400 OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN status = 'failed' OR status_code >= 400 OR error_message IS NOT NULL
THEN 1 ELSE 0 END), 0),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND user_id IS NOT NULL AND user_id <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY user_id
ON CONFLICT (hour_utc, user_id) DO UPDATE SET
total_requests = excluded.total_requests,
success_requests = excluded.success_requests,
error_requests = excluded.error_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
total_cost = excluded.total_cost,
updated_at = excluded.updated_at
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_hourly_user_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_user_model (
id, hour_utc, user_id, model, total_requests, input_tokens, output_tokens,
total_cost, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, user_id, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND user_id IS NOT NULL AND user_id <> ''
AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY user_id, model
ON CONFLICT (hour_utc, user_id, model) DO UPDATE SET
total_requests = excluded.total_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
total_cost = excluded.total_cost,
updated_at = excluded.updated_at
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_hourly_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_model (
id, hour_utc, model, total_requests, input_tokens, output_tokens, total_cost,
avg_response_time_ms, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL),
CAST(COALESCE(AVG(response_time_ms), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY model
ON CONFLICT (hour_utc, model) DO UPDATE SET
total_requests = excluded.total_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
total_cost = excluded.total_cost,
avg_response_time_ms = excluded.avg_response_time_ms,
updated_at = excluded.updated_at
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_hourly_provider_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_hourly_provider (
id, hour_utc, provider_name, total_requests, input_tokens, output_tokens,
total_cost, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, provider_name, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY provider_name
ON CONFLICT (hour_utc, provider_name) DO UPDATE SET
total_requests = excluded.total_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
total_cost = excluded.total_cost,
updated_at = excluded.updated_at
"#,
)
.bind(hour_utc)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_daily_model_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_model (
id, "date", model, total_requests, input_tokens, output_tokens,
cache_creation_tokens, cache_read_tokens, total_cost, avg_response_time_ms,
created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, model, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(cache_creation_input_tokens), 0),
COALESCE(SUM(cache_read_input_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL),
CAST(COALESCE(AVG(response_time_ms), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND model IS NOT NULL AND model <> ''
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY model
ON CONFLICT ("date", model) DO UPDATE SET
total_requests = excluded.total_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
avg_response_time_ms = excluded.avg_response_time_ms,
updated_at = excluded.updated_at
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_daily_provider_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_provider (
id, "date", provider_name, total_requests, input_tokens, output_tokens,
cache_creation_tokens, cache_read_tokens, total_cost, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, provider_name, COUNT(*),
COALESCE(SUM(input_tokens), 0), COALESCE(SUM(output_tokens), 0),
COALESCE(SUM(cache_creation_input_tokens), 0),
COALESCE(SUM(cache_read_input_tokens), 0),
CAST(COALESCE(SUM(total_cost_usd), 0) AS REAL), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
GROUP BY provider_name
ON CONFLICT ("date", provider_name) DO UPDATE SET
total_requests = excluded.total_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
updated_at = excluded.updated_at
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_daily_api_key_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_daily_api_key (
id, api_key_id, "date", total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, api_key_name, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), usage.api_key_id, ?, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(usage.input_tokens), 0), COALESCE(SUM(usage.output_tokens), 0),
COALESCE(SUM(usage.cache_creation_input_tokens), 0),
COALESCE(SUM(usage.cache_read_input_tokens), 0),
CAST(COALESCE(SUM(usage.total_cost_usd), 0) AS REAL), MAX(api_keys.name), ?, ?
FROM "usage" AS usage
LEFT JOIN api_keys ON api_keys.id = usage.api_key_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.api_key_id IS NOT NULL AND usage.api_key_id <> ''
GROUP BY usage.api_key_id
ON CONFLICT ("date", api_key_id) DO UPDATE SET
total_requests = excluded.total_requests,
success_requests = excluded.success_requests,
error_requests = excluded.error_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
api_key_name = COALESCE(excluded.api_key_name, stats_daily_api_key.api_key_name),
updated_at = excluded.updated_at
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn refresh_sqlite_stats_daily_error_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
sqlx::query(r#"DELETE FROM stats_daily_error WHERE "date" = ?"#)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
let result = sqlx::query(
r#"
INSERT INTO stats_daily_error (
id, "date", error_category, provider_name, model, count, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), ?, error_category, provider_name, model,
COUNT(*), ?, ?
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND error_category IS NOT NULL AND error_category <> ''
GROUP BY error_category, provider_name, model
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn upsert_sqlite_stats_user_daily_rows(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let result = sqlx::query(
r#"
INSERT INTO stats_user_daily (
id, user_id, "date", total_requests, success_requests, error_requests,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
total_cost, username, created_at, updated_at
)
SELECT
lower(hex(randomblob(32))), usage.user_id, ?, COUNT(*),
COUNT(*) - COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE
WHEN usage.status = 'failed' OR usage.status_code >= 400
OR usage.error_message IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(usage.input_tokens), 0), COALESCE(SUM(usage.output_tokens), 0),
COALESCE(SUM(usage.cache_creation_input_tokens), 0),
COALESCE(SUM(usage.cache_read_input_tokens), 0),
CAST(COALESCE(SUM(usage.total_cost_usd), 0) AS REAL), MAX(users.username), ?, ?
FROM "usage" AS usage
LEFT JOIN users ON users.id = usage.user_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND usage.status NOT IN ('pending', 'streaming')
AND usage.provider_name NOT IN ('unknown', 'pending')
GROUP BY usage.user_id
ON CONFLICT ("date", user_id) DO UPDATE SET
total_requests = excluded.total_requests,
success_requests = excluded.success_requests,
error_requests = excluded.error_requests,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_read_tokens = excluded.cache_read_tokens,
total_cost = excluded.total_cost,
username = COALESCE(excluded.username, stats_user_daily.username),
updated_at = excluded.updated_at
"#,
)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(result.rows_affected()).unwrap_or(usize::MAX))
}
async fn sqlite_daily_fallback_count(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<i64, DataLayerError> {
let start_unix_ms = start_unix_secs.checked_mul(1000).ok_or_else(|| {
DataLayerError::InvalidInput("stats fallback window start overflows milliseconds".into())
})?;
let end_unix_ms = end_unix_secs.checked_mul(1000).ok_or_else(|| {
DataLayerError::InvalidInput("stats fallback window end overflows milliseconds".into())
})?;
sqlx::query_scalar(
r#"
SELECT COUNT(*)
FROM (
SELECT request_id
FROM request_candidates
WHERE created_at >= ? AND created_at < ?
AND status IN ('success', 'failed')
GROUP BY request_id
HAVING COUNT(id) > 1
)
"#,
)
.bind(start_unix_ms)
.bind(end_unix_ms)
.fetch_one(&mut **tx)
.await
.map_sql_err()
}
async fn sqlite_group_count(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
group_columns: &str,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<usize, DataLayerError> {
let not_empty = group_columns
.split(',')
.map(str::trim)
.map(|column| format!("{column} IS NOT NULL AND {column} <> ''"))
.collect::<Vec<_>>()
.join(" AND ");
let sql = format!(
r#"
SELECT COUNT(*)
FROM (
SELECT 1
FROM "usage"
WHERE created_at_unix_ms >= ?
AND created_at_unix_ms < ?
AND status NOT IN ('pending', 'streaming')
AND provider_name NOT IN ('unknown', 'pending')
AND {not_empty}
GROUP BY {group_columns}
)
"#
);
let count: i64 = sqlx::query_scalar(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_one(&mut **tx)
.await
.map_sql_err()?;
Ok(usize::try_from(count.max(0)).unwrap_or(usize::MAX))
}
@@ -1,986 +0,0 @@
use sqlx::Sqlite;
use crate::backend::stats_common::stats_id;
use crate::error::SqlResultExt;
use crate::DataLayerError;
const CACHE_5M: &str = r#"MAX(
COALESCE(usage.cache_creation_input_tokens_5m, 0),
COALESCE(usage.cache_creation_ephemeral_5m_input_tokens, 0)
)"#;
const CACHE_1H: &str = r#"MAX(
COALESCE(usage.cache_creation_input_tokens_1h, 0),
COALESCE(usage.cache_creation_ephemeral_1h_input_tokens, 0)
)"#;
const CACHE_CREATION: &str = r#"CASE
WHEN COALESCE(usage.cache_creation_input_tokens, 0) = 0
AND ({cache_5m} + {cache_1h}) > 0
THEN {cache_5m} + {cache_1h}
ELSE MAX(COALESCE(usage.cache_creation_input_tokens, 0), 0)
END"#;
const EFFECTIVE_INPUT: &str = r#"CASE
WHEN (
LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, '')) IN ('openai', 'gemini', 'google')
OR LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, '')) LIKE 'openai:%'
OR LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, '')) LIKE 'gemini:%'
OR LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, '')) LIKE 'google:%'
)
AND COALESCE(usage.input_tokens, 0) > 0
AND COALESCE(usage.cache_read_input_tokens, 0) > 0
THEN MAX(COALESCE(usage.input_tokens, 0) - COALESCE(usage.cache_read_input_tokens, 0), 0)
ELSE MAX(COALESCE(usage.input_tokens, 0), 0)
END"#;
const SUCCESS: &str = r#"CASE
WHEN usage.status <> 'failed'
AND (usage.status_code IS NULL OR usage.status_code < 400)
AND usage.error_message IS NULL
THEN 1 ELSE 0
END"#;
const AGGREGATABLE: &str = r#"usage.status NOT IN ('pending', 'streaming')
AND usage.provider_name NOT IN ('unknown', 'pending')"#;
const SETTLED: &str = r#"COALESCE(settlement.billing_status, usage.billing_status) = 'settled'
AND COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) > 0"#;
fn cache_creation_expr() -> String {
CACHE_CREATION
.replace("{cache_5m}", CACHE_5M)
.replace("{cache_1h}", CACHE_1H)
}
fn total_input_context_expr() -> String {
format!(
"({EFFECTIVE_INPUT}) + ({}) + MAX(COALESCE(usage.cache_read_input_tokens, 0), 0)",
cache_creation_expr()
)
}
fn total_tokens_expr() -> String {
format!(
r#"COALESCE(
NULLIF(MAX(COALESCE(usage.total_tokens, 0), 0), 0),
({EFFECTIVE_INPUT})
+ MAX(COALESCE(usage.output_tokens, 0), 0)
+ ({})
+ MAX(COALESCE(usage.cache_read_input_tokens, 0), 0),
0
)"#,
cache_creation_expr()
)
}
fn percentile_cont(sorted: &[i64], percentile: f64) -> Option<i64> {
if sorted.is_empty() {
return None;
}
let position = percentile * (sorted.len().saturating_sub(1) as f64);
let lower = position.floor() as usize;
let upper = position.ceil() as usize;
let fraction = position - lower as f64;
let value = sorted[lower] as f64 + (sorted[upper] - sorted[lower]) as f64 * fraction;
Some(value.round() as i64)
}
async fn load_percentiles(
tx: &mut sqlx::Transaction<'_, Sqlite>,
column: &str,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(Option<i64>, Option<i64>, Option<i64>), DataLayerError> {
let sql = format!(
r#"
SELECT {column}
FROM "usage"
WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ?
AND status = 'completed'
AND provider_name NOT IN ('unknown', 'pending')
AND {column} IS NOT NULL
ORDER BY {column}
"#
);
let values: Vec<i64> = sqlx::query_scalar(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.fetch_all(&mut **tx)
.await
.map_sql_err()?;
if values.len() < 10 {
return Ok((None, None, None));
}
Ok((
percentile_cont(&values, 0.50),
percentile_cont(&values, 0.90),
percentile_cont(&values, 0.99),
))
}
pub(super) async fn refresh_hourly(
tx: &mut sqlx::Transaction<'_, Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let sql = format!(
r#"
UPDATE stats_hourly AS target
SET
cache_hit_total_requests = aggregated.cache_hit_total_requests,
cache_hit_requests = aggregated.cache_hit_requests,
completed_total_requests = aggregated.completed_total_requests,
completed_cache_hit_requests = aggregated.completed_cache_hit_requests,
completed_input_tokens = aggregated.completed_input_tokens,
completed_cache_creation_tokens = aggregated.completed_cache_creation_tokens,
completed_cache_read_tokens = aggregated.completed_cache_read_tokens,
completed_total_input_context = aggregated.completed_total_input_context,
completed_cache_creation_cost = aggregated.completed_cache_creation_cost,
completed_cache_read_cost = aggregated.completed_cache_read_cost,
settled_total_cost = aggregated.settled_total_cost,
settled_total_requests = aggregated.settled_total_requests,
settled_input_tokens = aggregated.settled_input_tokens,
settled_output_tokens = aggregated.settled_output_tokens,
settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs,
response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples
FROM (
SELECT
COUNT(*) AS cache_hit_total_requests,
COALESCE(SUM(CASE WHEN COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN 1 ELSE 0 END), 0) AS completed_total_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' AND COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS completed_cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS completed_input_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({cache_creation}) ELSE 0 END), 0) AS completed_cache_creation_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS completed_cache_read_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({total_context}) ELSE 0 END), 0) AS completed_total_input_context,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_creation_cost,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_read_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN 1 ELSE 0 END), 0) AS response_time_samples
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
) AS aggregated
WHERE target.hour_utc = ?
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
refresh_hourly_user(tx, hour_utc, start_unix_secs, end_unix_secs).await?;
refresh_hourly_response_dimensions(tx, hour_utc, start_unix_secs, end_unix_secs).await
}
async fn refresh_hourly_user(
tx: &mut sqlx::Transaction<'_, Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let sql = format!(
r#"
UPDATE stats_hourly_user AS target
SET
cache_creation_tokens = aggregated.cache_creation_tokens,
cache_read_tokens = aggregated.cache_read_tokens,
actual_total_cost = aggregated.actual_total_cost,
response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples,
settled_total_cost = aggregated.settled_total_cost,
settled_total_requests = aggregated.settled_total_requests,
settled_input_tokens = aggregated.settled_input_tokens,
settled_output_tokens = aggregated.settled_output_tokens,
settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs
FROM (
SELECT
usage.user_id,
COALESCE(SUM({cache_creation}), 0) AS cache_creation_tokens,
COALESCE(SUM(MAX(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens,
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0) AS actual_total_cost,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id
) AS aggregated
WHERE target.hour_utc = ? AND target.user_id = aggregated.user_id
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_hourly_response_dimensions(
tx: &mut sqlx::Transaction<'_, Sqlite>,
hour_utc: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
for (table, dimensions, group_by, target_match) in [
(
"stats_hourly_model",
"usage.model AS model",
"usage.model",
"target.model = aggregated.model",
),
(
"stats_hourly_user_model",
"usage.user_id AS user_id, usage.model AS model",
"usage.user_id, usage.model",
"target.user_id = aggregated.user_id AND target.model = aggregated.model",
),
] {
let sql = format!(
r#"
UPDATE {table} AS target
SET response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples
FROM (
SELECT {dimensions},
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples
FROM "usage" AS usage
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND {AGGREGATABLE}
GROUP BY {group_by}
) AS aggregated
WHERE target.hour_utc = ? AND {target_match}
"#
);
sqlx::query(&sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(hour_utc)
.execute(&mut **tx)
.await
.map_sql_err()?;
}
Ok(())
}
pub(super) async fn refresh_daily(
tx: &mut sqlx::Transaction<'_, Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let response = load_percentiles(tx, "response_time_ms", start_unix_secs, end_unix_secs).await?;
let first_byte =
load_percentiles(tx, "first_byte_time_ms", start_unix_secs, end_unix_secs).await?;
refresh_daily_root(
tx,
day_start,
start_unix_secs,
end_unix_secs,
response,
first_byte,
)
.await?;
refresh_daily_existing_dimensions(tx, day_start, start_unix_secs, end_unix_secs).await?;
upsert_user_dimension(
tx,
"stats_user_daily_model",
"model",
"usage.model",
"usage.model IS NOT NULL AND usage.model <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_user_dimension(
tx,
"stats_user_daily_provider",
"provider_name",
"usage.provider_name",
"usage.provider_name IS NOT NULL AND usage.provider_name <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_user_dimension(
tx,
"stats_user_daily_api_format",
"api_format",
"LOWER(COALESCE(usage.endpoint_api_format, usage.api_format, ''))",
"COALESCE(usage.endpoint_api_format, usage.api_format, '') <> ''",
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
upsert_model_provider_rows(tx, day_start, start_unix_secs, end_unix_secs, now_unix_secs)
.await?;
upsert_cost_savings_rows(tx, day_start, start_unix_secs, end_unix_secs, now_unix_secs).await?;
refresh_user_summary(tx, end_unix_secs, now_unix_secs).await
}
async fn refresh_daily_root(
tx: &mut sqlx::Transaction<'_, Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
response: (Option<i64>, Option<i64>, Option<i64>),
first_byte: (Option<i64>, Option<i64>, Option<i64>),
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let sql = format!(
r#"
UPDATE stats_daily AS target
SET
effective_input_tokens = aggregated.effective_input_tokens,
total_input_context = aggregated.total_input_context,
response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples,
cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens,
input_cost = aggregated.input_cost,
output_cost = aggregated.output_cost,
cache_creation_cost = aggregated.cache_creation_cost,
cache_read_cost = aggregated.cache_read_cost,
cache_hit_total_requests = aggregated.cache_hit_total_requests,
cache_hit_requests = aggregated.cache_hit_requests,
completed_total_requests = aggregated.completed_total_requests,
completed_cache_hit_requests = aggregated.completed_cache_hit_requests,
completed_input_tokens = aggregated.completed_input_tokens,
completed_cache_creation_tokens = aggregated.completed_cache_creation_tokens,
completed_cache_read_tokens = aggregated.completed_cache_read_tokens,
completed_total_input_context = aggregated.completed_total_input_context,
completed_cache_creation_cost = aggregated.completed_cache_creation_cost,
completed_cache_read_cost = aggregated.completed_cache_read_cost,
settled_total_cost = aggregated.settled_total_cost,
settled_total_requests = aggregated.settled_total_requests,
settled_input_tokens = aggregated.settled_input_tokens,
settled_output_tokens = aggregated.settled_output_tokens,
settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs,
p50_response_time_ms = ?, p90_response_time_ms = ?, p99_response_time_ms = ?,
p50_first_byte_time_ms = ?, p90_first_byte_time_ms = ?, p99_first_byte_time_ms = ?
FROM (
SELECT
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN ({EFFECTIVE_INPUT}) ELSE 0 END), 0) AS effective_input_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN ({total_context}) ELSE 0 END), 0) AS total_input_context,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL AND {AGGREGATABLE} THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN {CACHE_5M} ELSE 0 END), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN {CACHE_1H} ELSE 0 END), 0) AS cache_creation_ephemeral_1h_tokens,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.input_cost_usd, 0) ELSE 0 END), 0) AS input_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.output_cost_usd, 0) ELSE 0 END), 0) AS output_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS cache_creation_cost,
COALESCE(SUM(CASE WHEN {AGGREGATABLE} THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS cache_read_cost,
COUNT(*) AS cache_hit_total_requests,
COALESCE(SUM(CASE WHEN COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN 1 ELSE 0 END), 0) AS completed_total_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' AND COALESCE(usage.cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS completed_cache_hit_requests,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS completed_input_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({cache_creation}) ELSE 0 END), 0) AS completed_cache_creation_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS completed_cache_read_tokens,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN ({total_context}) ELSE 0 END), 0) AS completed_total_input_context,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_creation_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_creation_cost,
COALESCE(SUM(CASE WHEN usage.status = 'completed' THEN COALESCE(usage.cache_read_cost_usd, 0) ELSE 0 END), 0) AS completed_cache_read_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
) AS aggregated
WHERE target."date" = ?
"#
);
sqlx::query(&sql)
.bind(response.0)
.bind(response.1)
.bind(response.2)
.bind(first_byte.0)
.bind(first_byte.1)
.bind(first_byte.2)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_daily_existing_dimensions(
tx: &mut sqlx::Transaction<'_, Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let daily_model_sql = format!(
r#"
UPDATE stats_daily_model AS target
SET response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples,
cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens
FROM (
SELECT usage.model,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM({CACHE_5M}), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM({CACHE_1H}), 0) AS cache_creation_ephemeral_1h_tokens
FROM "usage" AS usage
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND {AGGREGATABLE} AND usage.model IS NOT NULL AND usage.model <> ''
GROUP BY usage.model
) AS aggregated
WHERE target."date" = ? AND target.model = aggregated.model
"#
);
sqlx::query(&daily_model_sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
let user_sql = format!(
r#"
UPDATE stats_user_daily AS target
SET effective_input_tokens = aggregated.effective_input_tokens,
total_input_context = aggregated.total_input_context,
cache_creation_cost = aggregated.cache_creation_cost,
cache_read_cost = aggregated.cache_read_cost,
actual_total_cost = aggregated.actual_total_cost,
response_time_sum_ms = aggregated.response_time_sum_ms,
response_time_samples = aggregated.response_time_samples,
cache_creation_ephemeral_5m_tokens = aggregated.cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens = aggregated.cache_creation_ephemeral_1h_tokens,
settled_total_cost = aggregated.settled_total_cost,
settled_total_requests = aggregated.settled_total_requests,
settled_input_tokens = aggregated.settled_input_tokens,
settled_output_tokens = aggregated.settled_output_tokens,
settled_cache_creation_tokens = aggregated.settled_cache_creation_tokens,
settled_cache_read_tokens = aggregated.settled_cache_read_tokens,
settled_first_finalized_at_unix_secs = aggregated.settled_first_finalized_at_unix_secs,
settled_last_finalized_at_unix_secs = aggregated.settled_last_finalized_at_unix_secs
FROM (
SELECT usage.user_id,
COALESCE(SUM({EFFECTIVE_INPUT}), 0) AS effective_input_tokens,
COALESCE(SUM({total_context}), 0) AS total_input_context,
COALESCE(SUM(COALESCE(usage.cache_creation_cost_usd, 0)), 0) AS cache_creation_cost,
COALESCE(SUM(COALESCE(usage.cache_read_cost_usd, 0)), 0) AS cache_read_cost,
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0) AS actual_total_cost,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0) AS response_time_sum_ms,
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples,
COALESCE(SUM({CACHE_5M}), 0) AS cache_creation_ephemeral_5m_tokens,
COALESCE(SUM({CACHE_1H}), 0) AS cache_creation_ephemeral_1h_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0) ELSE 0 END), 0) AS settled_total_cost,
COALESCE(SUM(CASE WHEN {SETTLED} THEN 1 ELSE 0 END), 0) AS settled_total_requests,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.input_tokens, 0), 0) ELSE 0 END), 0) AS settled_input_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.output_tokens, 0), 0) ELSE 0 END), 0) AS settled_output_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN ({cache_creation}) ELSE 0 END), 0) AS settled_cache_creation_tokens,
COALESCE(SUM(CASE WHEN {SETTLED} THEN MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) ELSE 0 END), 0) AS settled_cache_read_tokens,
MIN(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_first_finalized_at_unix_secs,
MAX(CASE WHEN {SETTLED} THEN COALESCE(settlement.finalized_at, usage.finalized_at) END) AS settled_last_finalized_at_unix_secs
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id
) AS aggregated
WHERE target."date" = ? AND target.user_id = aggregated.user_id
"#
);
sqlx::query(&user_sql)
.bind(start_unix_secs)
.bind(end_unix_secs)
.bind(day_start)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn upsert_user_dimension(
tx: &mut sqlx::Transaction<'_, Sqlite>,
table: &str,
dimension_column: &str,
dimension_expr: &str,
dimension_filter: &str,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let cache_creation = cache_creation_expr();
let total_context = total_input_context_expr();
let total_tokens = total_tokens_expr();
let sql = format!(
r#"
INSERT INTO {table} (
id, user_id, username, "date", {dimension_column}, total_requests, success_requests,
input_tokens, effective_input_tokens, output_tokens, total_tokens, total_input_context,
cache_creation_tokens, cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens, cache_read_tokens, total_cost, actual_total_cost,
response_time_sum_ms, response_time_samples, successful_response_time_sum_ms,
successful_response_time_samples, created_at, updated_at
)
SELECT lower(hex(randomblob(32))), usage.user_id,
MAX(COALESCE(usage.username, users.username)), ?, {dimension_expr}, COUNT(*),
COALESCE(SUM({SUCCESS}), 0),
COALESCE(SUM(MAX(COALESCE(usage.input_tokens, 0), 0)), 0),
COALESCE(SUM({EFFECTIVE_INPUT}), 0),
COALESCE(SUM(MAX(COALESCE(usage.output_tokens, 0), 0)), 0),
COALESCE(SUM({total_tokens}), 0), COALESCE(SUM({total_context}), 0),
COALESCE(SUM({cache_creation}), 0), COALESCE(SUM({CACHE_5M}), 0),
COALESCE(SUM({CACHE_1H}), 0),
COALESCE(SUM(MAX(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(COALESCE(settlement.billing_actual_total_cost_usd, usage.actual_total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN ({SUCCESS}) = 1 AND usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN ({SUCCESS}) = 1 AND usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0),
?, ?
FROM "usage" AS usage
LEFT JOIN users ON users.id = usage.user_id
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND {dimension_filter} AND {AGGREGATABLE}
GROUP BY usage.user_id, {dimension_expr}
ON CONFLICT (user_id, "date", {dimension_column}) DO UPDATE SET
username = COALESCE(excluded.username, {table}.username),
total_requests = excluded.total_requests, success_requests = excluded.success_requests,
input_tokens = excluded.input_tokens, effective_input_tokens = excluded.effective_input_tokens,
output_tokens = excluded.output_tokens, total_tokens = excluded.total_tokens,
total_input_context = excluded.total_input_context,
cache_creation_tokens = excluded.cache_creation_tokens,
cache_creation_ephemeral_5m_tokens = excluded.cache_creation_ephemeral_5m_tokens,
cache_creation_ephemeral_1h_tokens = excluded.cache_creation_ephemeral_1h_tokens,
cache_read_tokens = excluded.cache_read_tokens, total_cost = excluded.total_cost,
actual_total_cost = excluded.actual_total_cost,
response_time_sum_ms = excluded.response_time_sum_ms,
response_time_samples = excluded.response_time_samples,
successful_response_time_sum_ms = excluded.successful_response_time_sum_ms,
successful_response_time_samples = excluded.successful_response_time_samples,
updated_at = excluded.updated_at
"#
);
sqlx::query(&sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn upsert_model_provider_rows(
tx: &mut sqlx::Transaction<'_, Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let total_tokens = total_tokens_expr();
let model_provider_sql = format!(
r#"
INSERT INTO stats_daily_model_provider (
id, "date", model, provider_name, total_requests, total_tokens, total_cost,
response_time_sum_ms, response_time_samples, created_at, updated_at
)
SELECT lower(hex(randomblob(32))), ?, usage.model, usage.provider_name, COUNT(*),
COALESCE(SUM({total_tokens}), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0), ?, ?
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.model IS NOT NULL AND usage.model <> '' AND {AGGREGATABLE}
GROUP BY usage.model, usage.provider_name
ON CONFLICT ("date", model, provider_name) DO UPDATE SET
total_requests = excluded.total_requests, total_tokens = excluded.total_tokens,
total_cost = excluded.total_cost, response_time_sum_ms = excluded.response_time_sum_ms,
response_time_samples = excluded.response_time_samples, updated_at = excluded.updated_at
"#
);
sqlx::query(&model_provider_sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
let user_model_provider_sql = format!(
r#"
INSERT INTO stats_user_daily_model_provider (
id, user_id, username, "date", model, provider_name, total_requests, total_tokens,
total_cost, response_time_sum_ms, response_time_samples, created_at, updated_at
)
SELECT lower(hex(randomblob(32))), usage.user_id, MAX(COALESCE(usage.username, users.username)),
?, usage.model, usage.provider_name, COUNT(*), COALESCE(SUM({total_tokens}), 0),
COALESCE(SUM(COALESCE(settlement.billing_total_cost_usd, usage.total_cost_usd, 0)), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN MAX(usage.response_time_ms, 0) ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN usage.response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0), ?, ?
FROM "usage" AS usage
LEFT JOIN users ON users.id = usage.user_id
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ?
AND usage.user_id IS NOT NULL AND usage.user_id <> ''
AND usage.model IS NOT NULL AND usage.model <> '' AND {AGGREGATABLE}
GROUP BY usage.user_id, usage.model, usage.provider_name
ON CONFLICT (user_id, "date", model, provider_name) DO UPDATE SET
username = COALESCE(excluded.username, stats_user_daily_model_provider.username),
total_requests = excluded.total_requests, total_tokens = excluded.total_tokens,
total_cost = excluded.total_cost, response_time_sum_ms = excluded.response_time_sum_ms,
response_time_samples = excluded.response_time_samples, updated_at = excluded.updated_at
"#
);
sqlx::query(&user_model_provider_sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn upsert_cost_savings_rows(
tx: &mut sqlx::Transaction<'_, Sqlite>,
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
for (table, dimensions) in [
("stats_daily_cost_savings", Vec::new()),
(
"stats_daily_cost_savings_provider",
vec![("provider_name", "COALESCE(usage.provider_name, '')")],
),
(
"stats_daily_cost_savings_model",
vec![("model", "COALESCE(usage.model, '')")],
),
(
"stats_daily_cost_savings_model_provider",
vec![
("model", "COALESCE(usage.model, '')"),
("provider_name", "COALESCE(usage.provider_name, '')"),
],
),
] {
upsert_cost_savings_dimension(
tx,
table,
false,
&dimensions,
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
}
for (table, dimensions) in [
("stats_user_daily_cost_savings", Vec::new()),
(
"stats_user_daily_cost_savings_provider",
vec![("provider_name", "COALESCE(usage.provider_name, '')")],
),
(
"stats_user_daily_cost_savings_model",
vec![("model", "COALESCE(usage.model, '')")],
),
(
"stats_user_daily_cost_savings_model_provider",
vec![
("model", "COALESCE(usage.model, '')"),
("provider_name", "COALESCE(usage.provider_name, '')"),
],
),
] {
upsert_cost_savings_dimension(
tx,
table,
true,
&dimensions,
day_start,
start_unix_secs,
end_unix_secs,
now_unix_secs,
)
.await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn upsert_cost_savings_dimension(
tx: &mut sqlx::Transaction<'_, Sqlite>,
table: &str,
per_user: bool,
dimensions: &[(&str, &str)],
day_start: i64,
start_unix_secs: i64,
end_unix_secs: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let dimension_columns = dimensions
.iter()
.map(|(column, _)| *column)
.collect::<Vec<_>>();
let dimension_exprs = dimensions
.iter()
.map(|(_, expression)| *expression)
.collect::<Vec<_>>();
let user_columns = if per_user { "user_id, username, " } else { "" };
let user_select = if per_user {
"usage.user_id, MAX(COALESCE(usage.username, users.username)), "
} else {
""
};
let user_join = if per_user {
"LEFT JOIN users ON users.id = usage.user_id"
} else {
""
};
let user_filter = if per_user {
"AND usage.user_id IS NOT NULL AND usage.user_id <> ''"
} else {
""
};
let mut conflict_columns = vec!["\"date\""];
let mut group_by = Vec::new();
if per_user {
conflict_columns.insert(0, "user_id");
group_by.push("usage.user_id");
}
conflict_columns.extend(dimension_columns.iter().copied());
group_by.extend(dimension_exprs.iter().copied());
let dimension_columns_sql = if dimension_columns.is_empty() {
String::new()
} else {
format!("{}, ", dimension_columns.join(", "))
};
let dimension_select_sql = if dimension_exprs.is_empty() {
String::new()
} else {
format!("{}, ", dimension_exprs.join(", "))
};
let group_by_sql = if group_by.is_empty() {
String::new()
} else {
format!("GROUP BY {}", group_by.join(", "))
};
let sql = format!(
r#"
INSERT INTO {table} (
id, {user_columns}"date", {dimension_columns_sql}cache_read_tokens,
cache_read_cost, cache_creation_cost, estimated_full_cost, created_at, updated_at
)
SELECT lower(hex(randomblob(32))), {user_select}?, {dimension_select_sql}
COALESCE(SUM(MAX(COALESCE(usage.cache_read_input_tokens, 0), 0)), 0),
COALESCE(SUM(COALESCE(usage.cache_read_cost_usd, 0)), 0),
COALESCE(SUM(COALESCE(usage.cache_creation_cost_usd, 0)), 0),
COALESCE(SUM(
COALESCE(settlement.input_price_per_1m, usage.input_price_per_1m, 0)
* MAX(COALESCE(usage.cache_read_input_tokens, 0), 0) / 1000000.0
), 0), ?, ?
FROM "usage" AS usage
LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = usage.request_id
{user_join}
WHERE usage.created_at_unix_ms >= ? AND usage.created_at_unix_ms < ? {user_filter}
{group_by_sql}
ON CONFLICT ({}) DO UPDATE SET
{}cache_read_tokens = excluded.cache_read_tokens,
cache_read_cost = excluded.cache_read_cost,
cache_creation_cost = excluded.cache_creation_cost,
estimated_full_cost = excluded.estimated_full_cost,
updated_at = excluded.updated_at
"#,
conflict_columns.join(", "),
if per_user {
format!("username = COALESCE(excluded.username, {table}.username), ")
} else {
String::new()
}
);
sqlx::query(&sql)
.bind(day_start)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(start_unix_secs)
.bind(end_unix_secs)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}
async fn refresh_user_summary(
tx: &mut sqlx::Transaction<'_, Sqlite>,
cutoff_date: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
sqlx::query(
r#"
INSERT INTO stats_user_summary (
id, user_id, username, cutoff_date, all_time_requests, all_time_success_requests,
all_time_error_requests, all_time_input_tokens, all_time_output_tokens,
all_time_cache_creation_tokens, all_time_cache_read_tokens, all_time_cost,
all_time_actual_cost, active_days, first_active_date, last_active_date,
created_at, updated_at
)
SELECT lower(hex(randomblob(32))), user_id, MAX(username), ?,
COALESCE(SUM(total_requests), 0), COALESCE(SUM(success_requests), 0),
COALESCE(SUM(error_requests), 0), COALESCE(SUM(input_tokens), 0),
COALESCE(SUM(output_tokens), 0), COALESCE(SUM(cache_creation_tokens), 0),
COALESCE(SUM(cache_read_tokens), 0), COALESCE(SUM(total_cost), 0),
COALESCE(SUM(actual_total_cost), 0),
COALESCE(SUM(CASE WHEN total_requests > 0 THEN 1 ELSE 0 END), 0),
MIN(CASE WHEN total_requests > 0 THEN "date" END),
MAX(CASE WHEN total_requests > 0 THEN "date" END), ?, ?
FROM stats_user_daily
WHERE "date" < ?
GROUP BY user_id
ON CONFLICT (user_id) DO UPDATE SET
username = COALESCE(excluded.username, stats_user_summary.username),
cutoff_date = excluded.cutoff_date, all_time_requests = excluded.all_time_requests,
all_time_success_requests = excluded.all_time_success_requests,
all_time_error_requests = excluded.all_time_error_requests,
all_time_input_tokens = excluded.all_time_input_tokens,
all_time_output_tokens = excluded.all_time_output_tokens,
all_time_cache_creation_tokens = excluded.all_time_cache_creation_tokens,
all_time_cache_read_tokens = excluded.all_time_cache_read_tokens,
all_time_cost = excluded.all_time_cost, all_time_actual_cost = excluded.all_time_actual_cost,
active_days = excluded.active_days, first_active_date = excluded.first_active_date,
last_active_date = excluded.last_active_date, updated_at = excluded.updated_at
"#,
)
.bind(cutoff_date)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(cutoff_date)
.execute(&mut **tx)
.await
.map_sql_err()?;
refresh_global_summary(tx, cutoff_date, now_unix_secs).await?;
Ok(())
}
async fn refresh_global_summary(
tx: &mut sqlx::Transaction<'_, Sqlite>,
cutoff_date: i64,
now_unix_secs: i64,
) -> Result<(), DataLayerError> {
let existing_id: Option<String> =
sqlx::query_scalar("SELECT id FROM stats_summary ORDER BY created_at, id LIMIT 1")
.fetch_optional(&mut **tx)
.await
.map_sql_err()?;
let summary_id = existing_id.unwrap_or_else(|| stats_id("stats-summary"));
sqlx::query(
r#"
INSERT INTO stats_summary (
id, cutoff_date, all_time_requests, all_time_success_requests,
all_time_error_requests, all_time_input_tokens, all_time_output_tokens,
all_time_cache_creation_tokens, all_time_cache_read_tokens, all_time_cost,
all_time_actual_cost, total_users, active_users, total_api_keys,
active_api_keys, created_at, updated_at
)
SELECT ?, ?, COALESCE(SUM(total_requests), 0), COALESCE(SUM(success_requests), 0),
COALESCE(SUM(error_requests), 0), COALESCE(SUM(input_tokens), 0),
COALESCE(SUM(output_tokens), 0), COALESCE(SUM(cache_creation_tokens), 0),
COALESCE(SUM(cache_read_tokens), 0), COALESCE(SUM(total_cost), 0),
COALESCE(SUM(actual_total_cost), 0),
(SELECT COUNT(*) FROM users),
(SELECT COUNT(*) FROM users WHERE is_active <> 0),
(SELECT COUNT(*) FROM api_keys),
(SELECT COUNT(*) FROM api_keys WHERE is_active <> 0), ?, ?
FROM stats_daily
WHERE "date" < ?
ON CONFLICT (id) DO UPDATE SET
cutoff_date = excluded.cutoff_date,
all_time_requests = excluded.all_time_requests,
all_time_success_requests = excluded.all_time_success_requests,
all_time_error_requests = excluded.all_time_error_requests,
all_time_input_tokens = excluded.all_time_input_tokens,
all_time_output_tokens = excluded.all_time_output_tokens,
all_time_cache_creation_tokens = excluded.all_time_cache_creation_tokens,
all_time_cache_read_tokens = excluded.all_time_cache_read_tokens,
all_time_cost = excluded.all_time_cost,
all_time_actual_cost = excluded.all_time_actual_cost,
total_users = excluded.total_users, active_users = excluded.active_users,
total_api_keys = excluded.total_api_keys, active_api_keys = excluded.active_api_keys,
updated_at = excluded.updated_at
"#,
)
.bind(summary_id)
.bind(cutoff_date)
.bind(now_unix_secs)
.bind(now_unix_secs)
.bind(cutoff_date)
.execute(&mut **tx)
.await
.map_sql_err()?;
Ok(())
}