use std::collections::{BTreeMap, HashSet}; use std::io::Read; use std::time::{SystemTime, UNIX_EPOCH}; use aether_ai_formats::UPSTREAM_IS_STREAM_KEY; use aether_data_contracts::repository::usage::UsageBodyField; use async_trait::async_trait; use flate2::read::GzDecoder; use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite}; use crate::error::SqlResultExt; use crate::{sqlite_optional_real, sqlite_real, SqlitePool}; use aether_data_contracts::repository::usage::{ strip_deprecated_usage_display_fields, usage_can_recover_terminal_failure, usage_request_metadata_client_family, PendingUsageCleanupSummary, ProviderApiKeyWindowUsageRequest, StoredProviderApiKeyUsageSummary, StoredProviderApiKeyWindowUsageSummary, StoredProviderUsageSummary, StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary, StoredUsageCacheAffinityIntervalRow, StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary, StoredUsageDailySummary, StoredUsageDashboardDailyBreakdownRow, StoredUsageDashboardProviderCount, StoredUsageDashboardSummary, StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary, StoredUsagePerformancePercentilesRow, StoredUsageProviderPerformance, StoredUsageProviderPerformanceProviderRow, StoredUsageProviderPerformanceSummary, StoredUsageProviderPerformanceTimelineRow, StoredUsageSettledCostSummary, StoredUsageTimeSeriesBucket, StoredUsageUserTotals, UpsertUsageRecord, UsageAuditAggregationGroupBy, UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery, UsageAuditSummaryQuery, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery, UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy, UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCleanupExecutionMode, UsageCleanupPreviewCounts, UsageCleanupSummary, UsageCleanupTargets, UsageCleanupWindow, UsageCostSavingsSummaryQuery, UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery, UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy, UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery, UsagePerformancePercentilesQuery, UsageProviderPerformanceQuery, UsageReadRepository, UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery, UsageWriteRepository, }; use aether_data_contracts::DataLayerError; mod cleanup; mod counters; mod http_capture; mod snapshots; const USAGE_COLUMNS: &str = r#" SELECT id, "usage".request_id, user_id, api_key_id, provider_name, model, target_model, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.selected_provider_id ELSE "usage".provider_id END AS provider_id, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.selected_endpoint_id ELSE "usage".provider_endpoint_id END AS provider_endpoint_id, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.selected_provider_api_key_id ELSE "usage".provider_api_key_id END AS provider_api_key_id, request_type, api_format, api_family, endpoint_kind, endpoint_api_format, provider_api_family, provider_endpoint_kind, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN COALESCE(usage_routing_snapshots.has_format_conversion, 0) ELSE COALESCE("usage".has_format_conversion, 0) END AS has_format_conversion, is_stream, upstream_is_stream, input_tokens, COALESCE(usage_settlement_snapshots.billing_output_tokens, "usage".output_tokens, 0) AS output_tokens, total_tokens, COALESCE( usage_settlement_snapshots.billing_cache_creation_tokens, CASE WHEN usage_settlement_snapshots.billing_cache_creation_5m_tokens IS NOT NULL OR usage_settlement_snapshots.billing_cache_creation_1h_tokens IS NOT NULL THEN COALESCE(usage_settlement_snapshots.billing_cache_creation_5m_tokens, 0) + COALESCE(usage_settlement_snapshots.billing_cache_creation_1h_tokens, 0) END, "usage".cache_creation_input_tokens, 0 ) AS cache_creation_input_tokens, COALESCE( usage_settlement_snapshots.billing_cache_creation_5m_tokens, "usage".cache_creation_ephemeral_5m_input_tokens, 0 ) AS cache_creation_ephemeral_5m_input_tokens, COALESCE( usage_settlement_snapshots.billing_cache_creation_1h_tokens, "usage".cache_creation_ephemeral_1h_input_tokens, 0 ) AS cache_creation_ephemeral_1h_input_tokens, COALESCE( usage_settlement_snapshots.billing_cache_read_tokens, "usage".cache_read_input_tokens, 0 ) AS cache_read_input_tokens, CAST(COALESCE( usage_settlement_snapshots.billing_cache_creation_cost_usd, "usage".cache_creation_cost_usd, 0 ) AS REAL) AS cache_creation_cost_usd, CAST(COALESCE( usage_settlement_snapshots.billing_cache_read_cost_usd, "usage".cache_read_cost_usd, 0 ) AS REAL) AS cache_read_cost_usd, CAST(COALESCE( usage_settlement_snapshots.output_price_per_1m, "usage".output_price_per_1m ) AS REAL) AS output_price_per_1m, CAST(COALESCE( usage_settlement_snapshots.billing_total_cost_usd, "usage".total_cost_usd, 0 ) AS REAL) AS total_cost_usd, CAST(COALESCE( usage_settlement_snapshots.billing_actual_total_cost_usd, "usage".actual_total_cost_usd, 0 ) AS REAL) AS actual_total_cost_usd, status_code, error_message, error_category, response_time_ms, first_byte_time_ms, status, COALESCE(usage_settlement_snapshots.billing_status, "usage".billing_status) AS billing_status, COALESCE(usage_http_audits.request_headers, "usage".request_headers) AS request_headers, "usage".request_body, "usage".request_body_compressed, COALESCE( usage_http_audits.provider_request_headers, "usage".provider_request_headers ) AS provider_request_headers, "usage".provider_request_body, "usage".provider_request_body_compressed, COALESCE(usage_http_audits.response_headers, "usage".response_headers) AS response_headers, "usage".response_body, "usage".response_body_compressed, COALESCE( usage_http_audits.client_response_headers, "usage".client_response_headers ) AS client_response_headers, "usage".client_response_body, "usage".client_response_body_compressed, usage_http_audits.request_body_ref AS http_request_body_ref, usage_http_audits.provider_request_body_ref AS http_provider_request_body_ref, usage_http_audits.response_body_ref AS http_response_body_ref, usage_http_audits.client_response_body_ref AS http_client_response_body_ref, usage_http_audits.request_body_state AS http_request_body_state, usage_http_audits.provider_request_body_state AS http_provider_request_body_state, usage_http_audits.response_body_state AS http_response_body_state, usage_http_audits.client_response_body_state AS http_client_response_body_state, request_metadata, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.candidate_id ELSE "usage".candidate_id END AS routing_candidate_id, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.candidate_index ELSE "usage".candidate_index END AS routing_candidate_index, "usage".username AS username, "usage".api_key_name AS api_key_name, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.key_name ELSE "usage".key_name END AS routing_key_name, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.planner_kind ELSE "usage".planner_kind END AS routing_planner_kind, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.route_family ELSE "usage".route_family END AS routing_route_family, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.route_kind ELSE "usage".route_kind END AS routing_route_kind, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.execution_path ELSE "usage".execution_path END AS routing_execution_path, CASE WHEN usage_routing_snapshots.request_id IS NOT NULL THEN usage_routing_snapshots.local_execution_runtime_miss_reason ELSE "usage".local_execution_runtime_miss_reason END AS routing_local_execution_runtime_miss_reason, usage_settlement_snapshots.billing_snapshot_schema_version AS settlement_billing_snapshot_schema_version, usage_settlement_snapshots.billing_snapshot_status AS settlement_billing_snapshot_status, CAST(usage_settlement_snapshots.rate_multiplier AS REAL) AS settlement_rate_multiplier, usage_settlement_snapshots.is_free_tier AS settlement_is_free_tier, CAST(usage_settlement_snapshots.input_price_per_1m AS REAL) AS settlement_input_price_per_1m, CAST(usage_settlement_snapshots.output_price_per_1m AS REAL) AS settlement_output_price_per_1m, CAST(usage_settlement_snapshots.cache_creation_price_per_1m AS REAL) AS settlement_cache_creation_price_per_1m, CAST(usage_settlement_snapshots.cache_read_price_per_1m AS REAL) AS settlement_cache_read_price_per_1m, CAST(usage_settlement_snapshots.price_per_request AS REAL) AS settlement_price_per_request, usage_settlement_snapshots.settlement_snapshot_schema_version AS settlement_snapshot_schema_version, usage_settlement_snapshots.settlement_snapshot AS settlement_snapshot, usage_settlement_snapshots.billing_dimensions AS settlement_billing_dimensions, usage_settlement_snapshots.billing_input_tokens AS settlement_billing_input_tokens, usage_settlement_snapshots.billing_effective_input_tokens AS settlement_billing_effective_input_tokens, usage_settlement_snapshots.billing_output_tokens AS settlement_billing_output_tokens, usage_settlement_snapshots.billing_cache_creation_tokens AS settlement_billing_cache_creation_tokens, usage_settlement_snapshots.billing_cache_creation_5m_tokens AS settlement_billing_cache_creation_5m_tokens, usage_settlement_snapshots.billing_cache_creation_1h_tokens AS settlement_billing_cache_creation_1h_tokens, usage_settlement_snapshots.billing_cache_read_tokens AS settlement_billing_cache_read_tokens, usage_settlement_snapshots.billing_total_input_context AS settlement_billing_total_input_context, CAST(usage_settlement_snapshots.billing_cache_creation_cost_usd AS REAL) AS settlement_billing_cache_creation_cost_usd, CAST(usage_settlement_snapshots.billing_cache_read_cost_usd AS REAL) AS settlement_billing_cache_read_cost_usd, CAST(usage_settlement_snapshots.billing_total_cost_usd AS REAL) AS settlement_billing_total_cost_usd, CAST(usage_settlement_snapshots.billing_actual_total_cost_usd AS REAL) AS settlement_billing_actual_total_cost_usd, usage_settlement_snapshots.billing_pricing_source AS settlement_billing_pricing_source, usage_settlement_snapshots.billing_rule_id AS settlement_billing_rule_id, usage_settlement_snapshots.billing_rule_version AS settlement_billing_rule_version, COALESCE(usage_settlement_snapshots.finalized_at, "usage".finalized_at) AS finalized_at_unix_secs, created_at_unix_ms, updated_at_unix_secs FROM "usage" LEFT JOIN usage_http_audits ON usage_http_audits.request_id = "usage".request_id LEFT JOIN usage_routing_snapshots ON usage_routing_snapshots.request_id = "usage".request_id LEFT JOIN usage_settlement_snapshots ON usage_settlement_snapshots.request_id = "usage".request_id "#; const UPSERT_USAGE_SQL: &str = r#" INSERT INTO "usage" ( request_id, id, user_id, api_key_id, provider_name, model, target_model, provider_id, provider_endpoint_id, provider_api_key_id, request_type, api_format, api_family, endpoint_kind, endpoint_api_format, provider_api_family, provider_endpoint_kind, has_format_conversion, is_stream, upstream_is_stream, input_tokens, output_tokens, total_tokens, cache_creation_input_tokens, cache_creation_ephemeral_5m_input_tokens, cache_creation_ephemeral_1h_input_tokens, cache_read_input_tokens, cache_creation_cost_usd, cache_read_cost_usd, output_price_per_1m, total_cost_usd, actual_total_cost_usd, status_code, error_message, error_category, response_time_ms, first_byte_time_ms, status, billing_status, request_metadata, candidate_id, candidate_index, key_name, planner_kind, route_family, route_kind, execution_path, local_execution_runtime_miss_reason, finalized_at, created_at_unix_ms, updated_at_unix_secs ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ON CONFLICT (request_id) DO UPDATE SET user_id = excluded.user_id, api_key_id = excluded.api_key_id, provider_name = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_name ELSE excluded.provider_name END, model = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".model ELSE excluded.model END, target_model = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".target_model ELSE excluded.target_model END, provider_id = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_id ELSE excluded.provider_id END, provider_endpoint_id = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_endpoint_id ELSE excluded.provider_endpoint_id END, provider_api_key_id = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_api_key_id ELSE excluded.provider_api_key_id END, request_type = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".request_type ELSE excluded.request_type END, api_format = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".api_format ELSE excluded.api_format END, api_family = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".api_family ELSE excluded.api_family END, endpoint_kind = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".endpoint_kind ELSE excluded.endpoint_kind END, endpoint_api_format = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".endpoint_api_format ELSE excluded.endpoint_api_format END, provider_api_family = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_api_family ELSE excluded.provider_api_family END, provider_endpoint_kind = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".provider_endpoint_kind ELSE excluded.provider_endpoint_kind END, has_format_conversion = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".has_format_conversion ELSE excluded.has_format_conversion END, is_stream = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".is_stream ELSE excluded.is_stream END, upstream_is_stream = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".upstream_is_stream ELSE excluded.upstream_is_stream END, input_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".input_tokens ELSE excluded.input_tokens END, output_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".output_tokens ELSE excluded.output_tokens END, total_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".total_tokens ELSE excluded.total_tokens END, cache_creation_input_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_creation_input_tokens ELSE excluded.cache_creation_input_tokens END, cache_creation_ephemeral_5m_input_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_creation_ephemeral_5m_input_tokens ELSE excluded.cache_creation_ephemeral_5m_input_tokens END, cache_creation_ephemeral_1h_input_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_creation_ephemeral_1h_input_tokens ELSE excluded.cache_creation_ephemeral_1h_input_tokens END, cache_read_input_tokens = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_read_input_tokens ELSE excluded.cache_read_input_tokens END, cache_creation_cost_usd = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_creation_cost_usd ELSE excluded.cache_creation_cost_usd END, cache_read_cost_usd = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".cache_read_cost_usd ELSE excluded.cache_read_cost_usd END, output_price_per_1m = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".output_price_per_1m ELSE excluded.output_price_per_1m END, total_cost_usd = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".total_cost_usd ELSE excluded.total_cost_usd END, actual_total_cost_usd = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".actual_total_cost_usd ELSE excluded.actual_total_cost_usd END, status_code = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".status_code WHEN "usage".status = 'streaming' AND excluded.status = 'pending' THEN "usage".status_code WHEN "usage".status = 'streaming' AND excluded.status = 'streaming' AND excluded.status_code IS NULL THEN "usage".status_code ELSE excluded.status_code END, error_message = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".error_message WHEN "usage".status = 'streaming' AND excluded.status = 'pending' THEN "usage".error_message ELSE excluded.error_message END, error_category = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".error_category WHEN "usage".status = 'streaming' AND excluded.status = 'pending' THEN "usage".error_category ELSE excluded.error_category END, response_time_ms = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".response_time_ms WHEN excluded.response_time_ms IS NULL OR excluded.response_time_ms = 0 THEN COALESCE("usage".response_time_ms, excluded.response_time_ms) ELSE excluded.response_time_ms END, first_byte_time_ms = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".first_byte_time_ms WHEN excluded.first_byte_time_ms IS NULL OR excluded.first_byte_time_ms = 0 THEN COALESCE("usage".first_byte_time_ms, excluded.first_byte_time_ms) ELSE excluded.first_byte_time_ms END, status = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".status WHEN "usage".status = 'streaming' AND excluded.status = 'pending' THEN "usage".status ELSE excluded.status END, billing_status = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".billing_status ELSE excluded.billing_status END, request_metadata = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".request_metadata ELSE excluded.request_metadata END, candidate_id = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".candidate_id WHEN excluded.status IN ('completed', 'failed', 'cancelled') THEN excluded.candidate_id ELSE COALESCE(excluded.candidate_id, "usage".candidate_id) END, candidate_index = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".candidate_index WHEN excluded.status IN ('completed', 'failed', 'cancelled') THEN excluded.candidate_index ELSE COALESCE(excluded.candidate_index, "usage".candidate_index) END, key_name = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".key_name WHEN excluded.status IN ('completed', 'failed', 'cancelled') THEN excluded.key_name ELSE COALESCE(excluded.key_name, "usage".key_name) END, planner_kind = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".planner_kind ELSE excluded.planner_kind END, route_family = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".route_family ELSE excluded.route_family END, route_kind = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".route_kind ELSE excluded.route_kind END, execution_path = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".execution_path ELSE excluded.execution_path END, local_execution_runtime_miss_reason = CASE WHEN ("usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming')) OR ("usage".status = 'streaming' AND excluded.status = 'pending') THEN "usage".local_execution_runtime_miss_reason ELSE excluded.local_execution_runtime_miss_reason END, finalized_at = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".finalized_at ELSE excluded.finalized_at END, updated_at_unix_secs = CASE WHEN "usage".status IN ('completed', 'failed', 'cancelled') AND excluded.status IN ('pending', 'streaming') THEN "usage".updated_at_unix_secs ELSE excluded.updated_at_unix_secs END "#; const SQLITE_FIRST_BYTE_BATCH_SIZE: usize = 32; const UPSERT_FIRST_BYTE_BATCH_PREFIX_SQL: &str = r#" INSERT INTO "usage" ( id, request_id, user_id, api_key_id, provider_name, model, target_model, provider_id, provider_endpoint_id, provider_api_key_id, request_type, api_format, api_family, endpoint_kind, endpoint_api_format, provider_api_family, provider_endpoint_kind, has_format_conversion, is_stream, upstream_is_stream, status_code, response_time_ms, first_byte_time_ms, status, billing_status, request_metadata, created_at, created_at_unix_ms, updated_at_unix_secs ) "#; const UPSERT_FIRST_BYTE_BATCH_UPDATE_PREFIX_SQL: &str = r#" ON CONFLICT (request_id) DO UPDATE SET user_id = COALESCE(excluded.user_id, "usage".user_id), api_key_id = COALESCE(excluded.api_key_id, "usage".api_key_id), provider_name = excluded.provider_name, model = excluded.model, target_model = COALESCE(excluded.target_model, "usage".target_model), provider_id = COALESCE(excluded.provider_id, "usage".provider_id), provider_endpoint_id = COALESCE(excluded.provider_endpoint_id, "usage".provider_endpoint_id), provider_api_key_id = COALESCE(excluded.provider_api_key_id, "usage".provider_api_key_id), request_type = COALESCE(excluded.request_type, "usage".request_type), api_format = COALESCE(excluded.api_format, "usage".api_format), api_family = COALESCE(excluded.api_family, "usage".api_family), endpoint_kind = COALESCE(excluded.endpoint_kind, "usage".endpoint_kind), endpoint_api_format = COALESCE(excluded.endpoint_api_format, "usage".endpoint_api_format), provider_api_family = COALESCE(excluded.provider_api_family, "usage".provider_api_family), provider_endpoint_kind = COALESCE( excluded.provider_endpoint_kind, "usage".provider_endpoint_kind ), has_format_conversion = "#; const UPSERT_FIRST_BYTE_BATCH_UPDATE_SUFFIX_SQL: &str = r#", is_stream = 1, upstream_is_stream = COALESCE( CASE json_type(excluded.request_metadata, '$.upstream_is_stream') WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE NULL END, "usage".upstream_is_stream, "usage".is_stream, 1 ), status_code = COALESCE(excluded.status_code, "usage".status_code), response_time_ms = CASE WHEN excluded.response_time_ms IS NULL OR excluded.response_time_ms = 0 THEN "usage".response_time_ms ELSE excluded.response_time_ms END, first_byte_time_ms = CASE WHEN "usage".first_byte_time_ms IS NOT NULL AND "usage".first_byte_time_ms <> 0 THEN "usage".first_byte_time_ms WHEN excluded.first_byte_time_ms IS NULL OR excluded.first_byte_time_ms = 0 THEN "usage".first_byte_time_ms ELSE excluded.first_byte_time_ms END, status = 'streaming', request_metadata = COALESCE("usage".request_metadata, excluded.request_metadata), updated_at_unix_secs = MAX( COALESCE(NULLIF("usage".updated_at_unix_secs, 0), 0), COALESCE(NULLIF(excluded.updated_at_unix_secs, 0), 0), COALESCE("usage".created_at_unix_ms, 0) ) WHERE "usage".billing_status = 'pending' AND "usage".status IN ('pending', 'streaming') AND "usage".finalized_at IS NULL "#; const SELECT_STALE_PENDING_USAGE_BATCH_SQL: &str = r#" SELECT "usage".request_id, "usage".status, COALESCE(usage_settlement_snapshots.billing_status, "usage".billing_status) AS billing_status FROM "usage" LEFT JOIN usage_settlement_snapshots ON usage_settlement_snapshots.request_id = "usage".request_id WHERE "usage".status IN ('pending', 'streaming') AND "usage".created_at_unix_ms < ? ORDER BY "usage".created_at_unix_ms ASC, "usage".request_id ASC LIMIT ? "#; const SELECT_COMPLETED_REQUEST_CANDIDATES_SQL: &str = r#" SELECT status, extra_data FROM request_candidates WHERE request_id = ? AND status IN ('streaming', 'success') "#; const SQLITE_PROVIDER_IDENTITY_IS_NOT_RESERVED: &str = r#" ( ( provider_id IS NOT NULL AND TRIM(provider_id) <> '' AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'unknow', 'pending') ) OR ( provider_name IS NOT NULL AND TRIM(provider_name) <> '' AND LOWER(TRIM(provider_name)) NOT IN ('unknown', 'unknow', 'pending') ) ) "#; const SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR: &str = r#" CASE WHEN COALESCE(cache_creation_input_tokens, 0) = 0 AND ( COALESCE(cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE(cache_creation_ephemeral_1h_input_tokens, 0) ) > 0 THEN COALESCE(cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE(cache_creation_ephemeral_1h_input_tokens, 0) ELSE MAX(COALESCE(cache_creation_input_tokens, 0), 0) END "#; const SQLITE_USAGE_EFFECTIVE_INPUT_TOKENS_EXPR: &str = r#" CASE WHEN ( LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'openai' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'openai:%' ) AND COALESCE(input_tokens, 0) > 0 AND ( COALESCE(cache_creation_input_tokens, 0) > 0 OR COALESCE(cache_read_input_tokens, 0) > 0 ) THEN MAX( COALESCE(input_tokens, 0) - COALESCE(cache_creation_input_tokens, 0) - COALESCE(cache_read_input_tokens, 0), 0 ) WHEN ( LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'gemini' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'gemini:%' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'google' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'google:%' ) AND COALESCE(input_tokens, 0) > 0 AND COALESCE(cache_read_input_tokens, 0) > 0 THEN MAX(COALESCE(input_tokens, 0) - COALESCE(cache_read_input_tokens, 0), 0) ELSE MAX(COALESCE(input_tokens, 0), 0) END "#; const SQLITE_USAGE_TOTAL_INPUT_CONTEXT_EXPR: &str = r#" CASE WHEN ( LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'openai' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'openai:%' ) THEN ( CASE WHEN COALESCE(input_tokens, 0) > 0 AND ( COALESCE(cache_creation_input_tokens, 0) > 0 OR COALESCE(cache_read_input_tokens, 0) > 0 ) THEN MAX( COALESCE(input_tokens, 0) - COALESCE(cache_creation_input_tokens, 0) - COALESCE(cache_read_input_tokens, 0), 0 ) ELSE MAX(COALESCE(input_tokens, 0), 0) END ) + MAX(COALESCE(cache_creation_input_tokens, 0), 0) + MAX(COALESCE(cache_read_input_tokens, 0), 0) WHEN ( LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'gemini' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'gemini:%' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) = 'google' OR LOWER(COALESCE(endpoint_api_format, api_format, '')) LIKE 'google:%' ) THEN ( CASE WHEN COALESCE(input_tokens, 0) > 0 AND COALESCE(cache_read_input_tokens, 0) > 0 THEN MAX(COALESCE(input_tokens, 0) - COALESCE(cache_read_input_tokens, 0), 0) ELSE MAX(COALESCE(input_tokens, 0), 0) END ) + MAX(COALESCE(cache_read_input_tokens, 0), 0) ELSE MAX(COALESCE(input_tokens, 0), 0) + ( CASE WHEN COALESCE(cache_creation_input_tokens, 0) = 0 AND ( COALESCE(cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE(cache_creation_ephemeral_1h_input_tokens, 0) ) > 0 THEN COALESCE(cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE(cache_creation_ephemeral_1h_input_tokens, 0) ELSE MAX(COALESCE(cache_creation_input_tokens, 0), 0) END ) + MAX(COALESCE(cache_read_input_tokens, 0), 0) END "#; const SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR: &str = r#" MAX( COALESCE( CASE WHEN settlement.billing_effective_input_tokens IS NOT NULL THEN MAX(settlement.billing_effective_input_tokens, 0) + MAX(COALESCE(settlement.billing_output_tokens, "usage".output_tokens, 0), 0) + MAX( COALESCE( settlement.billing_cache_creation_tokens, CASE WHEN settlement.billing_cache_creation_5m_tokens IS NOT NULL OR settlement.billing_cache_creation_1h_tokens IS NOT NULL THEN COALESCE(settlement.billing_cache_creation_5m_tokens, 0) + COALESCE(settlement.billing_cache_creation_1h_tokens, 0) END, CASE WHEN COALESCE("usage".cache_creation_input_tokens, 0) = 0 AND ( COALESCE("usage".cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE("usage".cache_creation_ephemeral_1h_input_tokens, 0) ) > 0 THEN COALESCE("usage".cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE("usage".cache_creation_ephemeral_1h_input_tokens, 0) ELSE COALESCE("usage".cache_creation_input_tokens, 0) END, 0 ), 0 ) + MAX( COALESCE( settlement.billing_cache_read_tokens, "usage".cache_read_input_tokens, 0 ), 0 ) WHEN settlement.billing_total_input_context IS NOT NULL THEN MAX(settlement.billing_total_input_context, 0) + MAX(COALESCE(settlement.billing_output_tokens, "usage".output_tokens, 0), 0) END, NULLIF(MAX(COALESCE("usage".total_tokens, 0), 0), 0), ( CASE WHEN ( LOWER(COALESCE("usage".endpoint_api_format, "usage".api_format, '')) = 'openai' OR LOWER(COALESCE("usage".endpoint_api_format, "usage".api_format, '')) LIKE 'openai:%' OR LOWER(COALESCE("usage".endpoint_api_format, "usage".api_format, '')) = 'gemini' OR LOWER(COALESCE("usage".endpoint_api_format, "usage".api_format, '')) LIKE 'gemini:%' OR LOWER(COALESCE("usage".endpoint_api_format, "usage".api_format, '')) = 'google' 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 + MAX(COALESCE("usage".output_tokens, 0), 0) + ( CASE WHEN COALESCE("usage".cache_creation_input_tokens, 0) = 0 AND ( COALESCE("usage".cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE("usage".cache_creation_ephemeral_1h_input_tokens, 0) ) > 0 THEN COALESCE("usage".cache_creation_ephemeral_5m_input_tokens, 0) + COALESCE("usage".cache_creation_ephemeral_1h_input_tokens, 0) ELSE MAX(COALESCE("usage".cache_creation_input_tokens, 0), 0) END ) + MAX(COALESCE("usage".cache_read_input_tokens, 0), 0) ), 0 ), 0 ) "#; const SQLITE_USAGE_SUCCESS_FLAG_EXPR: &str = r#" CASE WHEN status <> 'failed' AND (status_code IS NULL OR status_code < 400) AND error_message IS NULL THEN 1 ELSE 0 END "#; const SQLITE_PROVIDER_KEY_SUCCESS_FLAG_EXPR: &str = r#" CASE WHEN status IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND (error_message IS NULL OR TRIM(error_message) = '') THEN 1 ELSE 0 END "#; const SQLITE_PROVIDER_KEY_ERROR_FLAG_EXPR: &str = r#" CASE WHEN status NOT IN ('pending', 'streaming') AND NOT ( status IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND (error_message IS NULL OR TRIM(error_message) = '') ) THEN 1 ELSE 0 END "#; const SQLITE_MONITORING_ERROR_PREDICATE: &str = r#" ( LOWER(TRIM(COALESCE(status, ''))) IN ('failed', 'error') OR (error_category IS NOT NULL AND TRIM(error_category) <> '') OR ( TRIM(COALESCE(status, '')) = '' AND ( COALESCE(status_code, 0) >= 400 OR (error_message IS NOT NULL AND TRIM(error_message) <> '') ) ) ) "#; const SQLITE_FINALIZED_USAGE_PREDICATE: &str = r#" status NOT IN ('pending', 'streaming') AND provider_name NOT IN ('unknown', 'pending') "#; fn push_sqlite_usage_where(builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool) { builder.push(if *has_where { " AND " } else { " WHERE " }); *has_where = true; } fn push_sqlite_usage_list_filters( builder: &mut QueryBuilder<'_, Sqlite>, query: &UsageAuditListQuery, has_where: &mut bool, ) { if let Some(created_from_unix_secs) = query.created_from_unix_secs { push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms >= ") .push_bind(created_from_unix_secs as i64); } if let Some(created_until_unix_secs) = query.created_until_unix_secs { push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms < ") .push_bind(created_until_unix_secs as i64); } if let Some(user_id) = query.user_id.as_deref() { push_sqlite_usage_where(builder, has_where); builder.push("user_id = ").push_bind(user_id.to_string()); } if let Some(provider_name) = query.provider_name.as_deref() { push_sqlite_usage_where(builder, has_where); builder .push("provider_name = ") .push_bind(provider_name.to_string()); } if let Some(model) = query.model.as_deref() { push_sqlite_usage_where(builder, has_where); builder.push("model = ").push_bind(model.to_string()); } if let Some(api_format) = query.api_format.as_deref() { push_sqlite_usage_where(builder, has_where); builder .push("api_format = ") .push_bind(api_format.to_string()); } if let Some(client_family) = query.client_family.as_deref().map(str::trim) { if !client_family.is_empty() { push_sqlite_usage_where(builder, has_where); builder .push("LOWER(COALESCE(NULLIF(TRIM(CAST(json_extract(request_metadata, '$.client_session_affinity.client_family') AS TEXT)), ''), NULLIF(TRIM(CAST(json_extract(request_metadata, '$.client_family') AS TEXT)), ''))) = ") .push_bind(client_family.to_ascii_lowercase()); } } if query.exclude_unknown_model_or_provider { push_sqlite_usage_where(builder, has_where); builder.push( "(LOWER(TRIM(COALESCE(model, ''))) NOT IN ('unknown', 'unknow') \ AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'unknow'))", ); } if let Some(statuses) = query.statuses.as_deref() { if !statuses.is_empty() { push_sqlite_usage_where(builder, has_where); builder.push("status IN ("); let mut separated = builder.separated(", "); for status in statuses { separated.push_bind(status.to_string()); } separated.push_unseparated(")"); } } push_sqlite_usage_excluded_status_codes(builder, has_where, &query.exclude_status_codes); if let Some(is_stream) = query.is_stream { push_sqlite_usage_where(builder, has_where); builder .push("is_stream = ") .push_bind(if is_stream { 1_i64 } else { 0_i64 }); } if let Some(is_websocket) = query.is_websocket { push_sqlite_usage_where(builder, has_where); builder .push("COALESCE(CAST(json_extract(request_metadata, '$.websocket_mode') AS INTEGER), 0) = ") .push_bind(if is_websocket { 1_i64 } else { 0_i64 }); } if query.error_only { push_sqlite_usage_where(builder, has_where); builder.push( "(status = 'failed' \ OR COALESCE(status_code, 0) >= 400 \ OR (error_message IS NOT NULL AND TRIM(error_message) <> ''))", ); } } fn push_sqlite_usage_excluded_status_codes( builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool, status_codes: &[u16], ) { if status_codes.is_empty() { return; } push_sqlite_usage_where(builder, has_where); builder.push("(status_code IS NULL OR status_code NOT IN ("); let mut separated = builder.separated(", "); for status_code in status_codes { separated.push_bind(i64::from(*status_code)); } separated.push_unseparated("))"); } fn push_sqlite_usage_keyword_filters( builder: &mut QueryBuilder<'_, Sqlite>, query: &UsageAuditKeywordSearchQuery, has_where: &mut bool, ) { push_sqlite_usage_list_filters( builder, &UsageAuditListQuery { created_from_unix_secs: query.created_from_unix_secs, created_until_unix_secs: query.created_until_unix_secs, user_id: query.user_id.clone(), provider_name: query.provider_name.clone(), model: query.model.clone(), api_format: query.api_format.clone(), client_family: query.client_family.clone(), exclude_unknown_model_or_provider: query.exclude_unknown_model_or_provider, statuses: query.statuses.clone(), exclude_status_codes: query.exclude_status_codes.clone(), is_stream: query.is_stream, is_websocket: query.is_websocket, error_only: query.error_only, limit: None, offset: None, newest_first: query.newest_first, }, has_where, ); for (index, keyword) in query.keywords.iter().enumerate() { let keyword = keyword.trim(); if keyword.is_empty() { continue; } let pattern = format!("%{}%", keyword.to_ascii_lowercase()); push_sqlite_usage_where(builder, has_where); builder.push("("); builder .push("LOWER(COALESCE(model, '')) LIKE ") .push_bind(pattern.clone()); builder .push(" OR LOWER(COALESCE(provider_name, '')) LIKE ") .push_bind(pattern.clone()); if query.auth_user_reader_available { let matched_user_ids = query .matched_user_ids_by_keyword .get(index) .cloned() .unwrap_or_default(); if !matched_user_ids.is_empty() { builder.push(" OR user_id IN ("); let mut separated = builder.separated(", "); for user_id in matched_user_ids { separated.push_bind(user_id); } separated.push_unseparated(")"); } } else { builder .push(" OR user_id IN (SELECT id FROM users WHERE LOWER(COALESCE(username, '')) LIKE ") .push_bind(pattern.clone()); builder.push(")"); } if query.auth_api_key_reader_available { let matched_api_key_ids = query .matched_api_key_ids_by_keyword .get(index) .cloned() .unwrap_or_default(); if !matched_api_key_ids.is_empty() { builder.push(" OR api_key_id IN ("); let mut separated = builder.separated(", "); for api_key_id in matched_api_key_ids { separated.push_bind(api_key_id); } separated.push_unseparated(")"); } } else { builder .push(" OR api_key_id IN (SELECT id FROM api_keys WHERE LOWER(COALESCE(name, '')) LIKE ") .push_bind(pattern); builder.push(")"); } builder.push(")"); } if let Some(username_keyword) = query .username_keyword .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { push_sqlite_usage_where(builder, has_where); if query.auth_user_reader_available { if query.matched_user_ids_for_username.is_empty() { builder.push("0 = 1"); } else { builder.push("user_id IN ("); let mut separated = builder.separated(", "); for user_id in &query.matched_user_ids_for_username { separated.push_bind(user_id.clone()); } separated.push_unseparated(")"); } } else { builder .push("user_id IN (SELECT id FROM users WHERE LOWER(COALESCE(username, '')) LIKE ") .push_bind(format!("%{}%", username_keyword.to_ascii_lowercase())); builder.push(")"); } } } fn push_sqlite_usage_summary_filters( builder: &mut QueryBuilder<'_, Sqlite>, query: &UsageAuditSummaryQuery, has_where: &mut bool, ) { push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms >= ") .push_bind(query.created_from_unix_secs as i64); push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms < ") .push_bind(query.created_until_unix_secs as i64); if let Some(user_id) = query.user_id.as_deref() { push_sqlite_usage_where(builder, has_where); builder.push("user_id = ").push_bind(user_id.to_string()); } if let Some(provider_name) = query.provider_name.as_deref() { push_sqlite_usage_where(builder, has_where); builder .push("provider_name = ") .push_bind(provider_name.to_string()); } if let Some(model) = query.model.as_deref() { push_sqlite_usage_where(builder, has_where); builder.push("model = ").push_bind(model.to_string()); } } fn push_sqlite_usage_order_limit_offset( builder: &mut QueryBuilder<'_, Sqlite>, newest_first: bool, limit: Option, offset: Option, ) { if newest_first { builder.push(" ORDER BY created_at_unix_ms DESC, id ASC"); } else { builder.push(" ORDER BY created_at_unix_ms ASC, \"usage\".request_id ASC"); } if let Some(limit) = limit { builder.push(" LIMIT ").push_bind(limit as i64); } if let Some(offset) = offset { builder.push(" OFFSET ").push_bind(offset as i64); } } fn sqlite_usage_aggregation_group_expr(group_by: UsageAuditAggregationGroupBy) -> &'static str { match group_by { UsageAuditAggregationGroupBy::Model => "COALESCE(NULLIF(model, ''), 'unknown')", UsageAuditAggregationGroupBy::Provider => { "CASE WHEN provider_id IS NOT NULL \ AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'unknow', 'pending') \ THEN TRIM(provider_id) ELSE TRIM(provider_name) END" } UsageAuditAggregationGroupBy::ApiFormat => "COALESCE(NULLIF(api_format, ''), 'unknown')", UsageAuditAggregationGroupBy::User => "user_id", } } fn sqlite_usage_aggregation_secondary_expr(group_by: UsageAuditAggregationGroupBy) -> &'static str { match group_by { UsageAuditAggregationGroupBy::Provider => { "CASE WHEN SUM(CASE WHEN provider_id IS NOT NULL \ AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'unknow', 'pending') \ THEN 1 ELSE 0 END) > 0 THEN 'provider_id' \ WHEN SUM(CASE WHEN provider_name IS NOT NULL \ AND TRIM(provider_name) <> '' \ AND LOWER(TRIM(provider_name)) NOT IN ('unknown', 'unknow', 'pending') \ THEN 1 ELSE 0 END) > 0 THEN 'legacy_name' \ ELSE NULL END" } _ => "NULL", } } fn sqlite_aggregate_u64(row: &SqliteRow, field: &str) -> Result { Ok(row.try_get::(field).map_sql_err()?.max(0) as u64) } fn sqlite_optional_u64(row: &SqliteRow, field: &str) -> Result, DataLayerError> { Ok(row .try_get::, _>(field) .map_sql_err()? .map(|value| value.max(0) as u64)) } fn push_sqlite_usage_provider_performance_base_filters( builder: &mut QueryBuilder<'_, Sqlite>, query: &UsageProviderPerformanceQuery, has_where: &mut bool, ) { push_sqlite_usage_range( builder, has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(builder, has_where); builder.push( "COALESCE(status, '') NOT IN ('pending', 'streaming') \ AND provider_id IS NOT NULL AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'pending') \ AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'pending')", ); push_sqlite_usage_provider_performance_filters(builder, query, has_where); } fn decode_sqlite_usage_audit_summary_row( row: &SqliteRow, ) -> Result { Ok(StoredUsageAuditSummary { total_requests: sqlite_aggregate_u64(row, "total_requests")?, input_tokens: sqlite_aggregate_u64(row, "input_tokens")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, recorded_total_tokens: sqlite_aggregate_u64(row, "recorded_total_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(row, "cache_creation_tokens")?, cache_creation_ephemeral_5m_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_5m_tokens", )?, cache_creation_ephemeral_1h_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_1h_tokens", )?, cache_read_tokens: sqlite_aggregate_u64(row, "cache_read_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(row, "actual_total_cost_usd")?, cache_creation_cost_usd: sqlite_real(row, "cache_creation_cost_usd")?, cache_read_cost_usd: sqlite_real(row, "cache_read_cost_usd")?, total_response_time_ms: sqlite_real(row, "total_response_time_ms")?, error_requests: sqlite_aggregate_u64(row, "error_requests")?, }) } fn decode_sqlite_usage_aggregation_row( row: &SqliteRow, ) -> Result { Ok(StoredUsageAuditAggregation { group_key: row.try_get::("group_key").map_sql_err()?, display_name: row.try_get("display_name").map_sql_err()?, secondary_name: row.try_get("secondary_name").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, effective_input_tokens: sqlite_aggregate_u64(row, "effective_input_tokens")?, total_input_context: sqlite_aggregate_u64(row, "total_input_context")?, cache_creation_tokens: sqlite_aggregate_u64(row, "cache_creation_tokens")?, cache_creation_ephemeral_5m_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_5m_tokens", )?, cache_creation_ephemeral_1h_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_1h_tokens", )?, cache_read_tokens: sqlite_aggregate_u64(row, "cache_read_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(row, "actual_total_cost_usd")?, avg_response_time_ms: sqlite_optional_real(row, "avg_response_time_ms")?, success_count: row .try_get::, _>("success_count") .map_sql_err()? .map(|value| value.max(0) as u64), }) } fn push_sqlite_usage_range( builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool, created_from_unix_secs: u64, created_until_unix_secs: u64, ) { push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms >= ") .push_bind(created_from_unix_secs as i64); push_sqlite_usage_where(builder, has_where); builder .push("created_at_unix_ms < ") .push_bind(created_until_unix_secs as i64); } fn push_sqlite_usage_optional_text_filter( builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool, column: &str, value: Option<&str>, ) { if let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) { push_sqlite_usage_where(builder, has_where); builder .push(column) .push(" = ") .push_bind(value.to_string()); } } fn push_sqlite_usage_bool_filter( builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool, column: &str, value: Option, ) { if let Some(value) = value { push_sqlite_usage_where(builder, has_where); builder .push(column) .push(" = ") .push_bind(if value { 1_i64 } else { 0_i64 }); } } fn push_sqlite_usage_finalized_filter( builder: &mut QueryBuilder<'_, Sqlite>, has_where: &mut bool, ) { push_sqlite_usage_where(builder, has_where); builder.push(SQLITE_FINALIZED_USAGE_PREDICATE); } fn push_sqlite_usage_provider_performance_filters( builder: &mut QueryBuilder<'_, Sqlite>, query: &UsageProviderPerformanceQuery, has_where: &mut bool, ) { push_sqlite_usage_optional_text_filter( builder, has_where, "provider_id", query.provider_id.as_deref(), ); push_sqlite_usage_optional_text_filter(builder, has_where, "model", query.model.as_deref()); push_sqlite_usage_optional_text_filter( builder, has_where, "api_format", query.api_format.as_deref(), ); push_sqlite_usage_optional_text_filter( builder, has_where, "endpoint_kind", query.endpoint_kind.as_deref(), ); push_sqlite_usage_bool_filter(builder, has_where, "is_stream", query.is_stream); push_sqlite_usage_bool_filter( builder, has_where, "has_format_conversion", query.has_format_conversion, ); } fn sqlite_usage_metadata_input_price_expr() -> &'static str { r#" COALESCE( CAST(json_extract(request_metadata, '$.input_price_per_1m') AS REAL), CAST(json_extract(request_metadata, '$.settlement_snapshot.input_price_per_1m') AS REAL), CAST(json_extract(request_metadata, '$.billing_snapshot.input_price_per_1m') AS REAL), 0 ) "# } fn sqlite_usage_bucket_expr( granularity: UsageTimeSeriesGranularity, tz_offset_minutes: i32, ) -> String { let offset = i64::from(tz_offset_minutes) * 60; match granularity { UsageTimeSeriesGranularity::Hour => { format!( "strftime('%Y-%m-%dT%H:00:00+00:00', created_at_unix_ms + ({offset}), 'unixepoch')" ) } UsageTimeSeriesGranularity::Day => { format!("date(created_at_unix_ms + ({offset}), 'unixepoch')") } } } fn sqlite_usage_local_date_expr(tz_offset_minutes: i32) -> String { let offset = i64::from(tz_offset_minutes) * 60; format!("date(created_at_unix_ms + ({offset}), 'unixepoch')") } fn sqlite_usage_breakdown_group_expr(group_by: UsageBreakdownGroupBy) -> &'static str { match group_by { UsageBreakdownGroupBy::Model => "COALESCE(NULLIF(model, ''), 'unknown')", UsageBreakdownGroupBy::Provider => "COALESCE(NULLIF(provider_name, ''), 'unknown')", UsageBreakdownGroupBy::ApiFormat => "COALESCE(NULLIF(api_format, ''), 'unknown')", } } fn sqlite_usage_leaderboard_group_expr( group_by: UsageLeaderboardGroupBy, ) -> (&'static str, &'static str, &'static str) { match group_by { UsageLeaderboardGroupBy::Model => { ("model", "NULL", "model IS NOT NULL AND TRIM(model) <> ''") } UsageLeaderboardGroupBy::User => ( "user_id", "(SELECT users.username FROM users WHERE users.id = user_id)", "user_id IS NOT NULL AND TRIM(user_id) <> ''", ), UsageLeaderboardGroupBy::ApiKey => ( "api_key_id", "(SELECT api_keys.name FROM api_keys WHERE api_keys.id = api_key_id)", "api_key_id IS NOT NULL AND TRIM(api_key_id) <> ''", ), } } fn sqlite_usage_body_sql_columns(field: UsageBodyField) -> (&'static str, &'static str) { match field { UsageBodyField::RequestBody => ("request_body", "request_body_compressed"), UsageBodyField::ProviderRequestBody => { ("provider_request_body", "provider_request_body_compressed") } UsageBodyField::ResponseBody => ("response_body", "response_body_compressed"), UsageBodyField::ClientResponseBody => { ("client_response_body", "client_response_body_compressed") } } } fn inflate_usage_json_value(bytes: &[u8]) -> Result { let mut decoder = GzDecoder::new(bytes); let mut json_bytes = Vec::new(); decoder.read_to_end(&mut json_bytes).map_err(|err| { DataLayerError::UnexpectedValue(format!("failed to decompress usage json: {err}")) })?; serde_json::from_slice(&json_bytes).map_err(|err| { DataLayerError::UnexpectedValue(format!("failed to parse decompressed usage json: {err}")) }) } fn parse_usage_json_text(raw: &str) -> Result { serde_json::from_str(raw).map_err(|err| { DataLayerError::UnexpectedValue(format!("failed to parse usage json: {err}")) }) } #[derive(Debug, Clone)] pub struct SqliteUsageWriteRepository { pool: SqlitePool, } #[derive(Debug)] struct PreparedFirstByteUsage { usage: UpsertUsageRecord, request_metadata_json: Option, response_time_ms: Option, first_byte_time_ms: Option, created_at_unix_secs: i64, updated_at_unix_secs: i64, } impl PreparedFirstByteUsage { fn try_from_usage(usage: UpsertUsageRecord) -> Result { usage.validate()?; if usage.status != "streaming" || usage.billing_status != "pending" { return Err(DataLayerError::InvalidInput( "first-byte usage upsert requires streaming status with pending billing" .to_string(), )); } let usage = strip_deprecated_usage_display_fields(usage); let request_metadata_json = usage .request_metadata .as_ref() .map(serde_json::to_string) .transpose() .map_err(|err| DataLayerError::InvalidInput(err.to_string()))?; let response_time_ms = usage .response_time_ms .map(|value| to_i64(value, "usage.response_time_ms")) .transpose()?; let first_byte_time_ms = usage .first_byte_time_ms .map(|value| to_i64(value, "usage.first_byte_time_ms")) .transpose()?; let created_at_unix_secs = to_i64( usage .created_at_unix_ms .unwrap_or(usage.updated_at_unix_secs), "usage.created_at_unix_ms", )?; let updated_at_unix_secs = to_i64(usage.updated_at_unix_secs, "usage.updated_at_unix_secs")?; Ok(Self { usage, request_metadata_json, response_time_ms, first_byte_time_ms, created_at_unix_secs, updated_at_unix_secs, }) } } #[derive(Debug, Clone)] pub struct SqliteUsageReadRepository { pool: SqlitePool, } #[derive(Debug, Clone, Copy, Default)] struct SqliteProviderPerformancePercentiles { p90_response_time_ms: Option, p99_response_time_ms: Option, p90_first_byte_time_ms: Option, p99_first_byte_time_ms: Option, } impl SqliteUsageReadRepository { pub fn new(pool: SqlitePool) -> Self { Self { pool } } async fn summarize_usage_daily_heatmap_raw_from_range( &self, created_from_unix_secs: u64, created_until_unix_secs: u64, user_id: Option<&str>, ) -> Result, DataLayerError> { let mut builder = QueryBuilder::::new(format!( r#" SELECT date(created_at_unix_ms, 'unixepoch') AS date, COUNT(*) AS requests, COALESCE(SUM( MAX(COALESCE(input_tokens, 0), 0) + MAX(COALESCE(output_tokens, 0), 0) + {cache_creation_expr} + MAX(COALESCE(cache_read_input_tokens, 0), 0) ), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost_usd AS REAL), 0)), 0) AS actual_total_cost_usd FROM "usage" "#, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_where(&mut builder, &mut has_where); builder .push("created_at_unix_ms >= ") .push_bind(created_from_unix_secs as i64); push_sqlite_usage_where(&mut builder, &mut has_where); builder .push("created_at_unix_ms < ") .push_bind(created_until_unix_secs as i64); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_optional_text_filter(&mut builder, &mut has_where, "user_id", user_id); builder.push(" GROUP BY date ORDER BY date ASC"); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter().map(map_sqlite_usage_daily_summary).collect() } async fn summarize_usage_daily_heatmap_from_daily_aggregates( &self, created_from_unix_secs: u64, created_until_unix_secs: u64, user_id: Option<&str>, ) -> Result, DataLayerError> { let rows = if let Some(user_id) = user_id { sqlx::query( r#" SELECT date("date", 'unixepoch') AS date, total_requests AS requests, input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens, total_cost AS total_cost_usd, total_cost AS actual_total_cost_usd FROM stats_user_daily WHERE user_id = ? AND "date" >= ? AND "date" < ? AND total_requests > 0 ORDER BY "date" ASC "#, ) .bind(user_id) .bind(created_from_unix_secs as i64) .bind(created_until_unix_secs as i64) .fetch_all(&self.pool) .await .map_sql_err()? } else { sqlx::query( r#" SELECT date("date", 'unixepoch') AS date, total_requests AS requests, input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens, total_cost AS total_cost_usd, actual_total_cost AS actual_total_cost_usd FROM stats_daily WHERE "date" >= ? AND "date" < ? AND total_requests > 0 ORDER BY "date" ASC "#, ) .bind(created_from_unix_secs as i64) .bind(created_until_unix_secs as i64) .fetch_all(&self.pool) .await .map_sql_err()? }; rows.iter().map(map_sqlite_usage_daily_summary).collect() } async fn summarize_provider_performance_percentiles( &self, query: &UsageProviderPerformanceQuery, ) -> Result { let mut builder = QueryBuilder::::new( r#" WITH filtered_usage AS ( SELECT MAX(COALESCE(response_time_ms, 0), 0) AS response_time_ms, MAX(COALESCE(first_byte_time_ms, 0), 0) AS first_byte_time_ms, response_time_ms IS NOT NULL AS has_response_time, first_byte_time_ms IS NOT NULL AS has_first_byte_time, CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END AS success_flag FROM "usage" "#, ); let mut has_where = false; push_sqlite_usage_provider_performance_base_filters(&mut builder, query, &mut has_where); builder.push( r#" ), response_ranked AS ( SELECT response_time_ms AS value, ROW_NUMBER() OVER (ORDER BY response_time_ms) AS rn, COUNT(response_time_ms) OVER () AS n FROM filtered_usage WHERE success_flag = 1 AND has_response_time ), first_byte_ranked AS ( SELECT first_byte_time_ms AS value, ROW_NUMBER() OVER (ORDER BY first_byte_time_ms) AS rn, COUNT(first_byte_time_ms) OVER () AS n FROM filtered_usage WHERE success_flag = 1 AND has_first_byte_time ), response_positions AS ( SELECT n, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM response_ranked GROUP BY n ), first_byte_positions AS ( SELECT n, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM first_byte_ranked GROUP BY n ), response_percentiles AS ( SELECT CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_response_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_response_time_ms FROM response_positions AS positions JOIN response_ranked AS ranked ON ranked.n = positions.n GROUP BY positions.n, positions.p90_pos, positions.p99_pos ), first_byte_percentiles AS ( SELECT CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_first_byte_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_first_byte_time_ms FROM first_byte_positions AS positions JOIN first_byte_ranked AS ranked ON ranked.n = positions.n GROUP BY positions.n, positions.p90_pos, positions.p99_pos ) SELECT (SELECT p90_response_time_ms FROM response_percentiles) AS p90_response_time_ms, (SELECT p99_response_time_ms FROM response_percentiles) AS p99_response_time_ms, (SELECT p90_first_byte_time_ms FROM first_byte_percentiles) AS p90_first_byte_time_ms, (SELECT p99_first_byte_time_ms FROM first_byte_percentiles) AS p99_first_byte_time_ms "#, ); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(SqliteProviderPerformancePercentiles { p90_response_time_ms: sqlite_optional_u64(&row, "p90_response_time_ms")?, p99_response_time_ms: sqlite_optional_u64(&row, "p99_response_time_ms")?, p90_first_byte_time_ms: sqlite_optional_u64(&row, "p90_first_byte_time_ms")?, p99_first_byte_time_ms: sqlite_optional_u64(&row, "p99_first_byte_time_ms")?, }) } async fn summarize_provider_performance_provider_percentiles( &self, query: &UsageProviderPerformanceQuery, provider_ids: &[String], ) -> Result, DataLayerError> { if provider_ids.is_empty() { return Ok(BTreeMap::new()); } let mut builder = QueryBuilder::::new( r#" WITH filtered_usage AS ( SELECT provider_id, MAX(COALESCE(response_time_ms, 0), 0) AS response_time_ms, MAX(COALESCE(first_byte_time_ms, 0), 0) AS first_byte_time_ms, response_time_ms IS NOT NULL AS has_response_time, first_byte_time_ms IS NOT NULL AS has_first_byte_time, CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END AS success_flag FROM "usage" "#, ); let mut has_where = false; push_sqlite_usage_provider_performance_base_filters(&mut builder, query, &mut has_where); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("provider_id IN ("); { let mut separated = builder.separated(", "); for provider_id in provider_ids { separated.push_bind(provider_id.clone()); } } builder.push( r#") ), response_ranked AS ( SELECT provider_id, response_time_ms AS value, ROW_NUMBER() OVER (PARTITION BY provider_id ORDER BY response_time_ms) AS rn, COUNT(response_time_ms) OVER (PARTITION BY provider_id) AS n FROM filtered_usage WHERE success_flag = 1 AND has_response_time ), first_byte_ranked AS ( SELECT provider_id, first_byte_time_ms AS value, ROW_NUMBER() OVER (PARTITION BY provider_id ORDER BY first_byte_time_ms) AS rn, COUNT(first_byte_time_ms) OVER (PARTITION BY provider_id) AS n FROM filtered_usage WHERE success_flag = 1 AND has_first_byte_time ), response_positions AS ( SELECT provider_id, n, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM response_ranked GROUP BY provider_id, n ), first_byte_positions AS ( SELECT provider_id, n, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM first_byte_ranked GROUP BY provider_id, n ), response_percentiles AS ( SELECT positions.provider_id, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_response_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_response_time_ms FROM response_positions AS positions JOIN response_ranked AS ranked ON ranked.provider_id = positions.provider_id AND ranked.n = positions.n GROUP BY positions.provider_id, positions.n, positions.p90_pos, positions.p99_pos ), first_byte_percentiles AS ( SELECT positions.provider_id, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_first_byte_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_first_byte_time_ms FROM first_byte_positions AS positions JOIN first_byte_ranked AS ranked ON ranked.provider_id = positions.provider_id AND ranked.n = positions.n GROUP BY positions.provider_id, positions.n, positions.p90_pos, positions.p99_pos ), provider_ids AS ( SELECT DISTINCT provider_id FROM filtered_usage ) SELECT provider_ids.provider_id, response_percentiles.p90_response_time_ms, response_percentiles.p99_response_time_ms, first_byte_percentiles.p90_first_byte_time_ms, first_byte_percentiles.p99_first_byte_time_ms FROM provider_ids LEFT JOIN response_percentiles ON response_percentiles.provider_id = provider_ids.provider_id LEFT JOIN first_byte_percentiles ON first_byte_percentiles.provider_id = provider_ids.provider_id "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; let mut values = BTreeMap::new(); for row in rows { values.insert( row.try_get::("provider_id").map_sql_err()?, SqliteProviderPerformancePercentiles { p90_response_time_ms: sqlite_optional_u64(&row, "p90_response_time_ms")?, p99_response_time_ms: sqlite_optional_u64(&row, "p99_response_time_ms")?, p90_first_byte_time_ms: sqlite_optional_u64(&row, "p90_first_byte_time_ms")?, p99_first_byte_time_ms: sqlite_optional_u64(&row, "p99_first_byte_time_ms")?, }, ); } Ok(values) } async fn fetch_usage_items( &self, mut builder: QueryBuilder<'_, Sqlite>, ) -> Result, DataLayerError> { let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter().map(|row| map_usage_row(row, false)).collect() } pub async fn list_usage_audits( &self, query: &UsageAuditListQuery, ) -> Result, DataLayerError> { let mut builder = QueryBuilder::::new(USAGE_COLUMNS); let mut has_where = false; push_sqlite_usage_list_filters(&mut builder, query, &mut has_where); push_sqlite_usage_order_limit_offset( &mut builder, query.newest_first, query.limit, query.offset, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter().map(|row| map_usage_row(row, false)).collect() } pub async fn count_usage_audits( &self, query: &UsageAuditListQuery, ) -> Result { let mut builder = QueryBuilder::::new(r#"SELECT COUNT(*) AS total FROM "usage""#); let mut has_where = false; push_sqlite_usage_list_filters(&mut builder, query, &mut has_where); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(row.try_get::("total").map_sql_err()?.max(0) as u64) } pub async fn list_usage_audits_by_keyword_search( &self, query: &UsageAuditKeywordSearchQuery, ) -> Result, DataLayerError> { let mut builder = QueryBuilder::::new(USAGE_COLUMNS); let mut has_where = false; push_sqlite_usage_keyword_filters(&mut builder, query, &mut has_where); push_sqlite_usage_order_limit_offset( &mut builder, query.newest_first, query.limit, query.offset, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter().map(|row| map_usage_row(row, false)).collect() } pub async fn count_usage_audits_by_keyword_search( &self, query: &UsageAuditKeywordSearchQuery, ) -> Result { let mut builder = QueryBuilder::::new(r#"SELECT COUNT(*) AS total FROM "usage""#); let mut has_where = false; push_sqlite_usage_keyword_filters(&mut builder, query, &mut has_where); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(row.try_get::("total").map_sql_err()?.max(0) as u64) } pub async fn summarize_usage_audits( &self, query: &UsageAuditSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageAuditSummary::default()); } let mut builder = QueryBuilder::::new(format!( r#" SELECT COUNT(*) AS total_requests, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM({total_tokens_expr}), 0) AS recorded_total_tokens, COALESCE(SUM({cache_creation_expr}), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_5m_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_5m_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_1h_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_1h_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost_usd AS REAL), 0)), 0) AS actual_total_cost_usd, COALESCE(SUM(COALESCE(CAST(cache_creation_cost_usd AS REAL), 0)), 0) AS cache_creation_cost_usd, COALESCE(SUM(COALESCE(CAST(cache_read_cost_usd AS REAL), 0)), 0) AS cache_read_cost_usd, COALESCE(SUM(MAX(COALESCE(response_time_ms, 0), 0)), 0) AS total_response_time_ms, COALESCE(SUM( CASE WHEN COALESCE(status_code, 0) >= 400 OR (error_message IS NOT NULL AND TRIM(error_message) <> '') THEN 1 ELSE 0 END ), 0) AS error_requests FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_summary_filters(&mut builder, query, &mut has_where); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; decode_sqlite_usage_audit_summary_row(&row) } pub async fn aggregate_usage_audits( &self, query: &UsageAuditAggregationQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs || query.limit == 0 { return Ok(Vec::new()); } let group_expr = sqlite_usage_aggregation_group_expr(query.group_by); let secondary_expr = sqlite_usage_aggregation_secondary_expr(query.group_by); let display_expr = if matches!(query.group_by, UsageAuditAggregationGroupBy::Provider) { "CASE WHEN provider_name IS NOT NULL AND TRIM(provider_name) <> '' AND LOWER(TRIM(provider_name)) NOT IN ('unknown', 'unknow', 'pending') THEN TRIM(provider_name) ELSE NULL END" } else { "NULL" }; let avg_response_expr = if matches!( query.group_by, UsageAuditAggregationGroupBy::Provider | UsageAuditAggregationGroupBy::ApiFormat ) { "CASE WHEN COUNT(*) = 0 THEN 0 ELSE COALESCE(SUM(MAX(COALESCE(response_time_ms, 0), 0)), 0) * 1.0 / COUNT(*) END" } else { "NULL" }; let success_count_expr = if matches!(query.group_by, UsageAuditAggregationGroupBy::Provider) { "COALESCE(SUM(CASE WHEN status IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END), 0)" } else { "NULL" }; let mut builder = QueryBuilder::::new(format!( r#" SELECT {group_expr} AS group_key, {display_expr} AS display_name, {secondary_expr} AS secondary_name, COUNT(*) AS request_count, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM({effective_input_expr}), 0) AS effective_input_tokens, COALESCE(SUM({total_input_context_expr}), 0) AS total_input_context, COALESCE(SUM({cache_creation_expr}), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_5m_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_5m_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_1h_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_1h_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost_usd AS REAL), 0)), 0) AS actual_total_cost_usd, {avg_response_expr} AS avg_response_time_ms, {success_count_expr} AS success_count FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, effective_input_expr = SQLITE_USAGE_EFFECTIVE_INPUT_TOKENS_EXPR, total_input_context_expr = SQLITE_USAGE_TOTAL_INPUT_CONTEXT_EXPR, secondary_expr = secondary_expr, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_where(&mut builder, &mut has_where); builder .push("created_at_unix_ms >= ") .push_bind(query.created_from_unix_secs as i64); push_sqlite_usage_where(&mut builder, &mut has_where); builder .push("created_at_unix_ms < ") .push_bind(query.created_until_unix_secs as i64); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("status NOT IN ('pending', 'streaming')"); if query.exclude_reserved_provider_labels { push_sqlite_usage_where(&mut builder, &mut has_where); builder.push(SQLITE_PROVIDER_IDENTITY_IS_NOT_RESERVED); } if matches!(query.group_by, UsageAuditAggregationGroupBy::User) { push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("user_id IS NOT NULL AND TRIM(user_id) <> ''"); } builder .push(" GROUP BY group_key") .push(" ORDER BY request_count DESC, group_key ASC") .push(" LIMIT ") .push_bind(query.limit as i64); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(decode_sqlite_usage_aggregation_row) .collect() } pub async fn summarize_usage_totals_by_user_ids( &self, user_ids: &[String], ) -> Result, DataLayerError> { if user_ids.is_empty() { return Ok(Vec::new()); } let unique_user_ids = user_ids .iter() .cloned() .collect::>(); let mut totals = BTreeMap::::new(); let mut aggregate_cutoffs = BTreeMap::::new(); let mut aggregate_builder = QueryBuilder::::new( r#" SELECT user_id, COALESCE(SUM(total_requests), 0) AS request_count, COALESCE( SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0 ) AS total_tokens, MAX("date") AS latest_date FROM stats_user_daily WHERE user_id IN ( "#, ); { let mut separated = aggregate_builder.separated(", "); for user_id in &unique_user_ids { separated.push_bind(user_id.clone()); } } aggregate_builder.push(") GROUP BY user_id ORDER BY user_id ASC"); let aggregate_rows = aggregate_builder .build() .fetch_all(&self.pool) .await .map_sql_err()?; for row in aggregate_rows { let user_id: String = row.try_get("user_id").map_sql_err()?; let latest_date = row.try_get::("latest_date").map_sql_err()?.max(0) as u64; aggregate_cutoffs.insert(user_id.clone(), latest_date.saturating_add(86_400)); totals.insert( user_id.clone(), StoredUsageUserTotals { user_id, request_count: row_u64(&row, "request_count")?, total_tokens: row_u64(&row, "total_tokens")?, }, ); } let mut builder = QueryBuilder::::new(format!( r#" SELECT "usage".user_id, COUNT(*) AS request_count, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens FROM "usage" JOIN ( "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); for (index, user_id) in unique_user_ids.iter().enumerate() { if index > 0 { builder.push(" UNION ALL "); } let cutoff = aggregate_cutoffs.get(user_id).copied().unwrap_or_default(); builder .push("SELECT ") .push_bind(user_id.clone()) .push(" AS user_id, ") .push_bind(to_i64(cutoff, "usage aggregate cutoff")?) .push(" AS cutoff_unix_secs"); } builder.push( r#" ) AS requested ON requested.user_id = "usage".user_id LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".created_at_unix_ms >= requested.cutoff_unix_secs AND "usage".status NOT IN ('pending', 'streaming') AND "usage".provider_name NOT IN ('unknown', 'pending') GROUP BY "usage".user_id ORDER BY "usage".user_id ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; for row in rows { let user_id: String = row.try_get("user_id").map_sql_err()?; let entry = totals .entry(user_id.clone()) .or_insert_with(|| StoredUsageUserTotals { user_id, request_count: 0, total_tokens: 0, }); entry.request_count = entry .request_count .saturating_add(row_u64(&row, "request_count")?); entry.total_tokens = entry .total_tokens .saturating_add(row_u64(&row, "total_tokens")?); } Ok(totals.into_values().collect()) } async fn summarize_dashboard_usage_from_daily_aggregates( &self, query: &UsageDashboardSummaryQuery, ) -> Result, DataLayerError> { let row = if let Some(user_id) = query.user_id.as_deref() { sqlx::query( r#" SELECT COALESCE(SUM(total_requests), 0) AS total_requests, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(input_tokens), 0) AS effective_input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(input_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_input_context, 0.0 AS cache_creation_cost_usd, 0.0 AS cache_read_cost_usd, COALESCE(SUM(COALESCE(CAST(total_cost AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(total_cost AS REAL), 0)), 0) AS actual_total_cost_usd, COALESCE(SUM(error_requests), 0) AS error_requests, 0.0 AS response_time_sum_ms, 0 AS response_time_samples FROM stats_user_daily WHERE user_id = ? AND "date" >= ? AND "date" < ? "#, ) .bind(user_id) .bind(query.created_from_unix_secs as i64) .bind(query.created_until_unix_secs as i64) .fetch_one(&self.pool) .await .map_sql_err()? } else { sqlx::query( r#" SELECT COALESCE(SUM(total_requests), 0) AS total_requests, COALESCE(SUM(input_tokens), 0) AS input_tokens, COALESCE(SUM(input_tokens), 0) AS effective_input_tokens, COALESCE(SUM(output_tokens), 0) AS output_tokens, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_tokens, COALESCE(SUM(cache_creation_tokens), 0) AS cache_creation_tokens, COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens, COALESCE(SUM(input_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_input_context, 0.0 AS cache_creation_cost_usd, 0.0 AS cache_read_cost_usd, COALESCE(SUM(COALESCE(CAST(total_cost AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost AS REAL), 0)), 0) AS actual_total_cost_usd, COALESCE(SUM(error_requests), 0) AS error_requests, 0.0 AS response_time_sum_ms, 0 AS response_time_samples FROM stats_daily WHERE "date" >= ? AND "date" < ? "#, ) .bind(query.created_from_unix_secs as i64) .bind(query.created_until_unix_secs as i64) .fetch_one(&self.pool) .await .map_sql_err()? }; let total_requests = sqlite_aggregate_u64(&row, "total_requests")?; if total_requests == 0 { return Ok(None); } Ok(Some(StoredUsageDashboardSummary { total_requests, input_tokens: sqlite_aggregate_u64(&row, "input_tokens")?, effective_input_tokens: sqlite_aggregate_u64(&row, "effective_input_tokens")?, output_tokens: sqlite_aggregate_u64(&row, "output_tokens")?, total_tokens: sqlite_aggregate_u64(&row, "total_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(&row, "cache_creation_tokens")?, cache_read_tokens: sqlite_aggregate_u64(&row, "cache_read_tokens")?, total_input_context: sqlite_aggregate_u64(&row, "total_input_context")?, cache_creation_cost_usd: sqlite_real(&row, "cache_creation_cost_usd")?, cache_read_cost_usd: sqlite_real(&row, "cache_read_cost_usd")?, total_cost_usd: sqlite_real(&row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(&row, "actual_total_cost_usd")?, error_requests: sqlite_aggregate_u64(&row, "error_requests")?, response_time_sum_ms: sqlite_real(&row, "response_time_sum_ms")?, response_time_samples: sqlite_aggregate_u64(&row, "response_time_samples")?, })) } async fn list_dashboard_daily_breakdown_from_daily_aggregates( &self, query: &UsageDashboardDailyBreakdownQuery, ) -> Result, DataLayerError> { let rows = if let Some(user_id) = query.user_id.as_deref() { sqlx::query( r#" SELECT date("date", 'unixepoch') AS date, 'aggregate' AS model, 'aggregate' AS provider, COALESCE(SUM(total_requests), 0) AS requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST(total_cost AS REAL), 0)), 0) AS total_cost_usd, 0.0 AS response_time_sum_ms, 0 AS response_time_samples FROM stats_user_daily WHERE user_id = ? AND "date" >= ? AND "date" < ? AND total_requests > 0 GROUP BY "date" ORDER BY "date" ASC "#, ) .bind(user_id) .bind(query.created_from_unix_secs as i64) .bind(query.created_until_unix_secs as i64) .fetch_all(&self.pool) .await .map_sql_err()? } else { sqlx::query( r#" SELECT date("date", 'unixepoch') AS date, 'aggregate' AS model, 'aggregate' AS provider, COALESCE(SUM(total_requests), 0) AS requests, COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST(total_cost AS REAL), 0)), 0) AS total_cost_usd, 0.0 AS response_time_sum_ms, 0 AS response_time_samples FROM stats_daily WHERE "date" >= ? AND "date" < ? AND total_requests > 0 GROUP BY "date" ORDER BY "date" ASC "#, ) .bind(query.created_from_unix_secs as i64) .bind(query.created_until_unix_secs as i64) .fetch_all(&self.pool) .await .map_sql_err()? }; rows.iter() .map(|row| { Ok(StoredUsageDashboardDailyBreakdownRow { date: row.try_get("date").map_sql_err()?, model: row.try_get("model").map_sql_err()?, provider: row.try_get("provider").map_sql_err()?, requests: sqlite_aggregate_u64(row, "requests")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, response_time_sum_ms: sqlite_real(row, "response_time_sum_ms")?, response_time_samples: sqlite_aggregate_u64(row, "response_time_samples")?, }) }) .collect() } } #[async_trait] impl UsageReadRepository for SqliteUsageReadRepository { async fn find_by_id( &self, id: &str, ) -> Result, DataLayerError> { let row = sqlx::query(&format!("{USAGE_COLUMNS} WHERE \"usage\".id = ? LIMIT 1")) .bind(id) .fetch_optional(&self.pool) .await .map_sql_err()?; row.as_ref() .map(|row| map_usage_row(row, false)) .transpose() } async fn list_by_ids( &self, ids: &[String], ) -> Result, DataLayerError> { if ids.is_empty() { return Ok(Vec::new()); } let mut builder = QueryBuilder::::new(USAGE_COLUMNS); builder.push(" WHERE \"usage\".id IN ("); { let mut separated = builder.separated(", "); for id in ids { separated.push_bind(id); } } builder.push(") ORDER BY created_at_unix_ms DESC, id ASC"); self.fetch_usage_items(builder).await } async fn find_by_request_id( &self, request_id: &str, ) -> Result, DataLayerError> { let row = sqlx::query(&format!( "{USAGE_COLUMNS} WHERE \"usage\".request_id = ? LIMIT 1" )) .bind(request_id) .fetch_optional(&self.pool) .await .map_sql_err()?; let usage = row .as_ref() .map(|row| map_usage_row(row, true)) .transpose()?; match usage { Some(usage) => http_capture::hydrate_usage_body_refs(&self.pool, usage) .await .map(Some), None => Ok(None), } } async fn resolve_body_ref( &self, body_ref: &str, ) -> Result, DataLayerError> { http_capture::resolve_body_ref(&self.pool, body_ref).await } async fn list_usage_audits( &self, query: &UsageAuditListQuery, ) -> Result, DataLayerError> { Self::list_usage_audits(self, query).await } async fn count_usage_audits(&self, query: &UsageAuditListQuery) -> Result { Self::count_usage_audits(self, query).await } async fn list_usage_audits_by_keyword_search( &self, query: &UsageAuditKeywordSearchQuery, ) -> Result, DataLayerError> { Self::list_usage_audits_by_keyword_search(self, query).await } async fn count_usage_audits_by_keyword_search( &self, query: &UsageAuditKeywordSearchQuery, ) -> Result { Self::count_usage_audits_by_keyword_search(self, query).await } async fn aggregate_usage_audits( &self, query: &UsageAuditAggregationQuery, ) -> Result, DataLayerError> { Self::aggregate_usage_audits(self, query).await } async fn summarize_usage_audits( &self, query: &UsageAuditSummaryQuery, ) -> Result { Self::summarize_usage_audits(self, query).await } async fn summarize_usage_totals_by_user_ids( &self, user_ids: &[String], ) -> Result, DataLayerError> { Self::summarize_usage_totals_by_user_ids(self, user_ids).await } async fn summarize_usage_cache_hit_summary( &self, query: &UsageCacheHitSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageCacheHitSummary::default()); } let mut builder = QueryBuilder::::new( r#" SELECT COUNT(*) AS total_requests, COALESCE(SUM(CASE WHEN COALESCE(cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS cache_hit_requests FROM "usage" "#, ); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(StoredUsageCacheHitSummary { total_requests: sqlite_aggregate_u64(&row, "total_requests")?, cache_hit_requests: sqlite_aggregate_u64(&row, "cache_hit_requests")?, }) } async fn summarize_usage_settled_cost( &self, query: &UsageSettledCostSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageSettledCostSummary::default()); } let mut builder = QueryBuilder::::new( r#" SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) AS total_cost_usd, COUNT(*) AS total_requests, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_input_tokens, 0), 0)), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, MIN(finalized_at) AS first_finalized_at_unix_secs, MAX(finalized_at) AS last_finalized_at_unix_secs FROM "usage" "#, ); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "api_key_id", query.api_key_id.as_deref(), ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("billing_status = 'settled' AND COALESCE(total_cost_usd, 0) > 0"); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(StoredUsageSettledCostSummary { total_cost_usd: sqlite_real(&row, "total_cost_usd")?, total_requests: sqlite_aggregate_u64(&row, "total_requests")?, input_tokens: sqlite_aggregate_u64(&row, "input_tokens")?, output_tokens: sqlite_aggregate_u64(&row, "output_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(&row, "cache_creation_tokens")?, cache_read_tokens: sqlite_aggregate_u64(&row, "cache_read_tokens")?, first_finalized_at_unix_secs: row .try_get::, _>("first_finalized_at_unix_secs") .map_sql_err()? .map(|value| value.max(0) as u64), last_finalized_at_unix_secs: row .try_get::, _>("last_finalized_at_unix_secs") .map_sql_err()? .map(|value| value.max(0) as u64), }) } async fn summarize_usage_cache_affinity_hit_summary( &self, query: &UsageCacheAffinityHitSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageCacheAffinityHitSummary::default()); } let mut builder = QueryBuilder::::new(format!( r#" SELECT COUNT(*) AS total_requests, COALESCE(SUM(CASE WHEN COALESCE(cache_read_input_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS requests_with_cache_hit, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM({cache_creation_expr}), 0) AS cache_creation_tokens, COALESCE(SUM({total_input_context_expr}), 0) AS total_input_context, COALESCE(SUM(COALESCE(CAST(cache_read_cost_usd AS REAL), 0)), 0) AS cache_read_cost_usd, COALESCE(SUM(COALESCE(CAST(cache_creation_cost_usd AS REAL), 0)), 0) AS cache_creation_cost_usd FROM "usage" "#, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR, total_input_context_expr = SQLITE_USAGE_TOTAL_INPUT_CONTEXT_EXPR )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("status = 'completed'"); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "api_key_id", query.api_key_id.as_deref(), ); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(StoredUsageCacheAffinityHitSummary { total_requests: sqlite_aggregate_u64(&row, "total_requests")?, requests_with_cache_hit: sqlite_aggregate_u64(&row, "requests_with_cache_hit")?, input_tokens: sqlite_aggregate_u64(&row, "input_tokens")?, cache_read_tokens: sqlite_aggregate_u64(&row, "cache_read_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(&row, "cache_creation_tokens")?, total_input_context: sqlite_aggregate_u64(&row, "total_input_context")?, cache_read_cost_usd: sqlite_real(&row, "cache_read_cost_usd")?, cache_creation_cost_usd: sqlite_real(&row, "cache_creation_cost_usd")?, }) } async fn list_usage_cache_affinity_intervals( &self, query: &UsageCacheAffinityIntervalQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let group_expr = match query.group_by { UsageCacheAffinityIntervalGroupBy::User => "user_id", UsageCacheAffinityIntervalGroupBy::ApiKey => "api_key_id", }; let mut builder = QueryBuilder::::new(format!( r#" WITH filtered_usage AS ( SELECT {group_expr} AS group_id, NULL AS username, model, created_at_unix_ms, id, LAG(created_at_unix_ms) OVER ( PARTITION BY {group_expr} ORDER BY created_at_unix_ms ASC, id ASC ) AS previous_created_at_unix_secs FROM "usage" "# )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "api_key_id", query.api_key_id.as_deref(), ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("status = 'completed'"); push_sqlite_usage_where(&mut builder, &mut has_where); builder .push(group_expr) .push(" IS NOT NULL AND TRIM(") .push(group_expr) .push(") <> ''"); builder.push( r#" ) SELECT group_id, username, model, created_at_unix_ms AS created_at_unix_secs, (created_at_unix_ms - previous_created_at_unix_secs) * 1.0 / 60.0 AS interval_minutes FROM filtered_usage WHERE previous_created_at_unix_secs IS NOT NULL ORDER BY created_at_unix_ms ASC, id ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageCacheAffinityIntervalRow { group_id: row.try_get("group_id").map_sql_err()?, username: row.try_get("username").map_sql_err()?, model: row.try_get("model").map_sql_err()?, created_at_unix_secs: sqlite_aggregate_u64(row, "created_at_unix_secs")?, interval_minutes: sqlite_real(row, "interval_minutes")?, }) }) .collect() } async fn summarize_dashboard_usage( &self, query: &UsageDashboardSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageDashboardSummary::default()); } if let Some(summary) = self .summarize_dashboard_usage_from_daily_aggregates(query) .await? { return Ok(summary); } let mut builder = QueryBuilder::::new(format!( r#" SELECT COUNT(*) AS total_requests, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM({effective_input_expr}), 0) AS effective_input_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM({cache_creation_expr}), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM({total_input_context_expr}), 0) AS total_input_context, COALESCE(SUM(COALESCE(CAST(cache_creation_cost_usd AS REAL), 0)), 0) AS cache_creation_cost_usd, COALESCE(SUM(COALESCE(CAST(cache_read_cost_usd AS REAL), 0)), 0) AS cache_read_cost_usd, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost_usd AS REAL), 0)), 0) AS actual_total_cost_usd, COALESCE(SUM(CASE WHEN COALESCE(status_code, 0) >= 400 OR status = 'failed' THEN 1 ELSE 0 END), 0) AS error_requests, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) AS response_time_sum_ms, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, effective_input_expr = SQLITE_USAGE_EFFECTIVE_INPUT_TOKENS_EXPR, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR, total_input_context_expr = SQLITE_USAGE_TOTAL_INPUT_CONTEXT_EXPR )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(StoredUsageDashboardSummary { total_requests: sqlite_aggregate_u64(&row, "total_requests")?, input_tokens: sqlite_aggregate_u64(&row, "input_tokens")?, effective_input_tokens: sqlite_aggregate_u64(&row, "effective_input_tokens")?, output_tokens: sqlite_aggregate_u64(&row, "output_tokens")?, total_tokens: sqlite_aggregate_u64(&row, "total_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(&row, "cache_creation_tokens")?, cache_read_tokens: sqlite_aggregate_u64(&row, "cache_read_tokens")?, total_input_context: sqlite_aggregate_u64(&row, "total_input_context")?, cache_creation_cost_usd: sqlite_real(&row, "cache_creation_cost_usd")?, cache_read_cost_usd: sqlite_real(&row, "cache_read_cost_usd")?, total_cost_usd: sqlite_real(&row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(&row, "actual_total_cost_usd")?, error_requests: sqlite_aggregate_u64(&row, "error_requests")?, response_time_sum_ms: sqlite_real(&row, "response_time_sum_ms")?, response_time_samples: sqlite_aggregate_u64(&row, "response_time_samples")?, }) } async fn list_dashboard_daily_breakdown( &self, query: &UsageDashboardDailyBreakdownQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let aggregate_rows = self .list_dashboard_daily_breakdown_from_daily_aggregates(query) .await?; if !aggregate_rows.is_empty() { return Ok(aggregate_rows); } let date_expr = sqlite_usage_local_date_expr(query.tz_offset_minutes); let mut builder = QueryBuilder::::new(format!( r#" SELECT {date_expr} AS date, model, provider_name AS provider, COUNT(*) AS requests, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) AS response_time_sum_ms, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); builder.push( r#" GROUP BY date, model, provider ORDER BY date ASC, total_cost_usd DESC, model ASC, provider ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageDashboardDailyBreakdownRow { date: row.try_get("date").map_sql_err()?, model: row.try_get("model").map_sql_err()?, provider: row.try_get("provider").map_sql_err()?, requests: sqlite_aggregate_u64(row, "requests")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, response_time_sum_ms: sqlite_real(row, "response_time_sum_ms")?, response_time_samples: sqlite_aggregate_u64(row, "response_time_samples")?, }) }) .collect() } async fn summarize_dashboard_provider_counts( &self, query: &UsageDashboardProviderCountsQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let mut builder = QueryBuilder::::new( r#" SELECT provider_name, COUNT(*) AS request_count FROM "usage" "#, ); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); builder.push( r#" GROUP BY provider_name ORDER BY request_count DESC, provider_name ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageDashboardProviderCount { provider_name: row.try_get("provider_name").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, }) }) .collect() } async fn summarize_usage_breakdown( &self, query: &UsageBreakdownSummaryQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let group_expr = sqlite_usage_breakdown_group_expr(query.group_by); let mut builder = QueryBuilder::::new(format!( r#" SELECT {group_expr} AS group_key, COUNT(*) AS request_count, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM({effective_input_expr}), 0) AS effective_input_tokens, COALESCE(SUM({total_input_context_expr}), 0) AS total_input_context, COALESCE(SUM({cache_creation_expr}), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_5m_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_5m_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_ephemeral_1h_input_tokens, 0), 0)), 0) AS cache_creation_ephemeral_1h_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(COALESCE(CAST(actual_total_cost_usd AS REAL), 0)), 0) AS actual_total_cost_usd, COALESCE(SUM({success_flag_expr}), 0) AS success_count, COALESCE(SUM(CASE WHEN {success_flag_expr} = 1 AND response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) AS response_time_sum_ms, COALESCE(SUM(CASE WHEN {success_flag_expr} = 1 AND response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_samples, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) AS overall_response_time_sum_ms, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS overall_response_time_samples FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, effective_input_expr = SQLITE_USAGE_EFFECTIVE_INPUT_TOKENS_EXPR, total_input_context_expr = SQLITE_USAGE_TOTAL_INPUT_CONTEXT_EXPR, cache_creation_expr = SQLITE_USAGE_CACHE_CREATION_TOKENS_EXPR, success_flag_expr = SQLITE_USAGE_SUCCESS_FLAG_EXPR, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "provider_name", query.provider_name.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "model", query.model.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "api_format", query.api_format.as_deref(), ); push_sqlite_usage_excluded_status_codes( &mut builder, &mut has_where, &query.exclude_status_codes, ); if matches!(query.group_by, UsageBreakdownGroupBy::ApiFormat) { push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("api_format IS NOT NULL"); } builder.push(" GROUP BY group_key ORDER BY request_count DESC, group_key ASC"); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageBreakdownSummaryRow { group_key: row.try_get("group_key").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, input_tokens: sqlite_aggregate_u64(row, "input_tokens")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, effective_input_tokens: sqlite_aggregate_u64(row, "effective_input_tokens")?, total_input_context: sqlite_aggregate_u64(row, "total_input_context")?, cache_creation_tokens: sqlite_aggregate_u64(row, "cache_creation_tokens")?, cache_creation_ephemeral_5m_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_5m_tokens", )?, cache_creation_ephemeral_1h_tokens: sqlite_aggregate_u64( row, "cache_creation_ephemeral_1h_tokens", )?, cache_read_tokens: sqlite_aggregate_u64(row, "cache_read_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(row, "actual_total_cost_usd")?, success_count: sqlite_aggregate_u64(row, "success_count")?, response_time_sum_ms: sqlite_real(row, "response_time_sum_ms")?, response_time_samples: sqlite_aggregate_u64(row, "response_time_samples")?, overall_response_time_sum_ms: sqlite_real(row, "overall_response_time_sum_ms")?, overall_response_time_samples: sqlite_aggregate_u64( row, "overall_response_time_samples", )?, }) }) .collect() } async fn count_monitoring_usage_errors( &self, query: &UsageMonitoringErrorCountQuery, ) -> Result { let row = sqlx::query(&format!( r#" SELECT COUNT(*) AS total FROM "usage" WHERE created_at_unix_ms >= ? AND created_at_unix_ms < ? AND {error_predicate} "#, error_predicate = SQLITE_MONITORING_ERROR_PREDICATE )) .bind(query.created_from_unix_secs as i64) .bind(query.created_until_unix_secs as i64) .fetch_one(&self.pool) .await .map_sql_err()?; sqlite_aggregate_u64(&row, "total") } async fn list_monitoring_usage_errors( &self, query: &UsageMonitoringErrorListQuery, ) -> Result, DataLayerError> { let mut builder = QueryBuilder::::new(USAGE_COLUMNS); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push(SQLITE_MONITORING_ERROR_PREDICATE); builder.push(" ORDER BY created_at_unix_ms DESC, id ASC"); if let Some(limit) = query.limit { builder.push(" LIMIT ").push_bind(limit as i64); } self.fetch_usage_items(builder).await } async fn summarize_usage_error_distribution( &self, query: &UsageErrorDistributionQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let date_expr = sqlite_usage_local_date_expr(query.tz_offset_minutes); let mut builder = QueryBuilder::::new(format!( r#" SELECT {date_expr} AS date, error_category, COUNT(*) AS count FROM "usage" "# )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("error_category IS NOT NULL AND TRIM(error_category) <> ''"); builder.push( r#" GROUP BY date, error_category ORDER BY date ASC, count DESC, error_category ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageErrorDistributionRow { date: row.try_get("date").map_sql_err()?, error_category: row.try_get("error_category").map_sql_err()?, count: sqlite_aggregate_u64(row, "count")?, }) }) .collect() } async fn summarize_usage_performance_percentiles( &self, query: &UsagePerformancePercentilesQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let date_expr = sqlite_usage_local_date_expr(query.tz_offset_minutes); let mut builder = QueryBuilder::::new(format!( r#" WITH filtered_usage AS ( SELECT {date_expr} AS date, response_time_ms, first_byte_time_ms FROM "usage" "# )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push("status = 'completed'"); builder.push( r#" ), response_ranked AS ( SELECT date, response_time_ms AS value, ROW_NUMBER() OVER (PARTITION BY date ORDER BY response_time_ms) AS rn, COUNT(response_time_ms) OVER (PARTITION BY date) AS n FROM filtered_usage WHERE response_time_ms IS NOT NULL ), first_byte_ranked AS ( SELECT date, first_byte_time_ms AS value, ROW_NUMBER() OVER (PARTITION BY date ORDER BY first_byte_time_ms) AS rn, COUNT(first_byte_time_ms) OVER (PARTITION BY date) AS n FROM filtered_usage WHERE first_byte_time_ms IS NOT NULL ), response_positions AS ( SELECT date, n, 0.5 * (n - 1) AS p50_pos, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM response_ranked GROUP BY date, n ), first_byte_positions AS ( SELECT date, n, 0.5 * (n - 1) AS p50_pos, 0.9 * (n - 1) AS p90_pos, 0.99 * (n - 1) AS p99_pos FROM first_byte_ranked GROUP BY date, n ), response_percentiles AS ( SELECT positions.date, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p50_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p50_pos = CAST(positions.p50_pos AS INTEGER) THEN CAST(positions.p50_pos AS INTEGER) + 1 ELSE CAST(positions.p50_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p50_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p50_pos - CAST(positions.p50_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p50_response_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_response_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_response_time_ms FROM response_positions AS positions JOIN response_ranked AS ranked ON ranked.date = positions.date GROUP BY positions.date, positions.n, positions.p50_pos, positions.p90_pos, positions.p99_pos ), first_byte_percentiles AS ( SELECT positions.date, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p50_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p50_pos = CAST(positions.p50_pos AS INTEGER) THEN CAST(positions.p50_pos AS INTEGER) + 1 ELSE CAST(positions.p50_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p50_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p50_pos - CAST(positions.p50_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p50_first_byte_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p90_pos = CAST(positions.p90_pos AS INTEGER) THEN CAST(positions.p90_pos AS INTEGER) + 1 ELSE CAST(positions.p90_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p90_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p90_pos - CAST(positions.p90_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p90_first_byte_time_ms, CASE WHEN positions.n >= 10 THEN CAST(( MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) + ( MAX(CASE WHEN ranked.rn = CASE WHEN positions.p99_pos = CAST(positions.p99_pos AS INTEGER) THEN CAST(positions.p99_pos AS INTEGER) + 1 ELSE CAST(positions.p99_pos AS INTEGER) + 2 END THEN ranked.value END) - MAX(CASE WHEN ranked.rn = CAST(positions.p99_pos AS INTEGER) + 1 THEN ranked.value END) ) * (positions.p99_pos - CAST(positions.p99_pos AS INTEGER)) ) AS INTEGER) ELSE NULL END AS p99_first_byte_time_ms FROM first_byte_positions AS positions JOIN first_byte_ranked AS ranked ON ranked.date = positions.date GROUP BY positions.date, positions.n, positions.p50_pos, positions.p90_pos, positions.p99_pos ), dates AS ( SELECT date FROM response_percentiles UNION SELECT date FROM first_byte_percentiles ) SELECT dates.date, response_percentiles.p50_response_time_ms, response_percentiles.p90_response_time_ms, response_percentiles.p99_response_time_ms, first_byte_percentiles.p50_first_byte_time_ms, first_byte_percentiles.p90_first_byte_time_ms, first_byte_percentiles.p99_first_byte_time_ms FROM dates LEFT JOIN response_percentiles ON response_percentiles.date = dates.date LEFT JOIN first_byte_percentiles ON first_byte_percentiles.date = dates.date ORDER BY dates.date ASC "#, ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsagePerformancePercentilesRow { date: row.try_get("date").map_sql_err()?, p50_response_time_ms: row .try_get::, _>("p50_response_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), p90_response_time_ms: row .try_get::, _>("p90_response_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), p99_response_time_ms: row .try_get::, _>("p99_response_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), p50_first_byte_time_ms: row .try_get::, _>("p50_first_byte_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), p90_first_byte_time_ms: row .try_get::, _>("p90_first_byte_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), p99_first_byte_time_ms: row .try_get::, _>("p99_first_byte_time_ms") .map_sql_err()? .map(|value| value.max(0) as u64), }) }) .collect() } async fn summarize_usage_provider_performance( &self, query: &UsageProviderPerformanceQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageProviderPerformance::default()); } let summary_sql = format!( r#" SELECT COUNT(*) AS request_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END), 0) AS success_count, CASE WHEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) > 0 THEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(output_tokens, 0), 0) ELSE 0 END), 0) * 1000.0 / COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) ELSE NULL END AS avg_output_tps, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND first_byte_time_ms IS NOT NULL THEN MAX(COALESCE(first_byte_time_ms, 0), 0) ELSE NULL END) AS avg_first_byte_time_ms, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE NULL END) AS avg_response_time_ms, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS tps_sample_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_sample_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND first_byte_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS first_byte_sample_count, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL AND response_time_ms >= {slow_threshold} THEN 1 ELSE 0 END), 0) AS slow_request_count FROM "usage" "#, slow_threshold = query.slow_threshold_ms ); let mut summary_builder = QueryBuilder::::new(summary_sql); let mut has_where = false; push_sqlite_usage_range( &mut summary_builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut summary_builder, &mut has_where); summary_builder.push( "COALESCE(status, '') NOT IN ('pending', 'streaming') \ AND provider_id IS NOT NULL AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'pending') \ AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'pending')", ); push_sqlite_usage_provider_performance_filters(&mut summary_builder, query, &mut has_where); let summary_row = summary_builder .build() .fetch_one(&self.pool) .await .map_sql_err()?; let summary_percentiles = self .summarize_provider_performance_percentiles(query) .await?; let summary = StoredUsageProviderPerformanceSummary { request_count: sqlite_aggregate_u64(&summary_row, "request_count")?, success_count: sqlite_aggregate_u64(&summary_row, "success_count")?, avg_output_tps: sqlite_optional_real(&summary_row, "avg_output_tps")?, avg_first_byte_time_ms: sqlite_optional_real(&summary_row, "avg_first_byte_time_ms")?, avg_response_time_ms: sqlite_optional_real(&summary_row, "avg_response_time_ms")?, p90_response_time_ms: summary_percentiles.p90_response_time_ms, p99_response_time_ms: summary_percentiles.p99_response_time_ms, p90_first_byte_time_ms: summary_percentiles.p90_first_byte_time_ms, p99_first_byte_time_ms: summary_percentiles.p99_first_byte_time_ms, tps_sample_count: sqlite_aggregate_u64(&summary_row, "tps_sample_count")?, response_time_sample_count: sqlite_aggregate_u64( &summary_row, "response_time_sample_count", )?, first_byte_sample_count: sqlite_aggregate_u64(&summary_row, "first_byte_sample_count")?, slow_request_count: sqlite_aggregate_u64(&summary_row, "slow_request_count")?, }; let provider_sql = format!( r#" SELECT provider_id, COALESCE(MAX(NULLIF(TRIM(provider_name), '')), provider_id) AS provider, COUNT(*) AS request_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END), 0) AS success_count, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, CASE WHEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) > 0 THEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(output_tokens, 0), 0) ELSE 0 END), 0) * 1000.0 / COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) ELSE NULL END AS avg_output_tps, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND first_byte_time_ms IS NOT NULL THEN MAX(COALESCE(first_byte_time_ms, 0), 0) ELSE NULL END) AS avg_first_byte_time_ms, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE NULL END) AS avg_response_time_ms, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN 1 ELSE 0 END), 0) AS tps_sample_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND response_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS response_time_sample_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND first_byte_time_ms IS NOT NULL THEN 1 ELSE 0 END), 0) AS first_byte_sample_count, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL AND response_time_ms >= {slow_threshold} THEN 1 ELSE 0 END), 0) AS slow_request_count FROM "usage" "#, slow_threshold = query.slow_threshold_ms ); let mut provider_builder = QueryBuilder::::new(provider_sql); let mut has_where = false; push_sqlite_usage_range( &mut provider_builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut provider_builder, &mut has_where); provider_builder.push( "COALESCE(status, '') NOT IN ('pending', 'streaming') \ AND provider_id IS NOT NULL AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'pending') \ AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'pending')", ); push_sqlite_usage_provider_performance_filters( &mut provider_builder, query, &mut has_where, ); provider_builder .push(" GROUP BY provider_id ORDER BY request_count DESC, provider_id ASC LIMIT "); provider_builder.push_bind(query.limit.max(1) as i64); let provider_rows = provider_builder .build() .fetch_all(&self.pool) .await .map_sql_err()?; let provider_ids = provider_rows .iter() .map(|row| row.try_get::("provider_id").map_sql_err()) .collect::, DataLayerError>>()?; let provider_percentiles = self .summarize_provider_performance_provider_percentiles(query, &provider_ids) .await?; let providers = provider_rows .iter() .map(|row| { let provider_id = row.try_get::("provider_id").map_sql_err()?; let percentiles = provider_percentiles .get(&provider_id) .copied() .unwrap_or_default(); Ok(StoredUsageProviderPerformanceProviderRow { provider_id, provider: row.try_get("provider").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, success_count: sqlite_aggregate_u64(row, "success_count")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, avg_output_tps: sqlite_optional_real(row, "avg_output_tps")?, avg_first_byte_time_ms: sqlite_optional_real(row, "avg_first_byte_time_ms")?, avg_response_time_ms: sqlite_optional_real(row, "avg_response_time_ms")?, p90_response_time_ms: percentiles.p90_response_time_ms, p99_response_time_ms: percentiles.p99_response_time_ms, p90_first_byte_time_ms: percentiles.p90_first_byte_time_ms, p99_first_byte_time_ms: percentiles.p99_first_byte_time_ms, tps_sample_count: sqlite_aggregate_u64(row, "tps_sample_count")?, response_time_sample_count: sqlite_aggregate_u64( row, "response_time_sample_count", )?, first_byte_sample_count: sqlite_aggregate_u64(row, "first_byte_sample_count")?, slow_request_count: sqlite_aggregate_u64(row, "slow_request_count")?, }) }) .collect::, DataLayerError>>()?; let timeline = if !query.include_timeline || provider_ids.is_empty() { Vec::new() } else { let bucket_expr = sqlite_usage_bucket_expr(query.granularity, query.tz_offset_minutes); let mut timeline_builder = QueryBuilder::::new(format!( r#" SELECT {bucket_expr} AS date, provider_id, COALESCE(MAX(NULLIF(TRIM(provider_name), '')), provider_id) AS provider, COUNT(*) AS request_count, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END), 0) AS success_count, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, CASE WHEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) > 0 THEN COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(output_tokens, 0), 0) ELSE 0 END), 0) * 1000.0 / COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND COALESCE(response_time_ms, 0) > 0 AND COALESCE(output_tokens, 0) > 0 THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE 0 END), 0) ELSE NULL END AS avg_output_tps, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND first_byte_time_ms IS NOT NULL THEN MAX(COALESCE(first_byte_time_ms, 0), 0) ELSE NULL END) AS avg_first_byte_time_ms, AVG(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) AND response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE NULL END) AS avg_response_time_ms, COALESCE(SUM(CASE WHEN response_time_ms IS NOT NULL AND response_time_ms >= {slow_threshold} THEN 1 ELSE 0 END), 0) AS slow_request_count FROM "usage" "#, slow_threshold = query.slow_threshold_ms )); let mut has_where = false; push_sqlite_usage_range( &mut timeline_builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_where(&mut timeline_builder, &mut has_where); timeline_builder.push( "COALESCE(status, '') NOT IN ('pending', 'streaming') \ AND provider_id IS NOT NULL AND TRIM(provider_id) <> '' \ AND LOWER(TRIM(provider_id)) NOT IN ('unknown', 'pending') \ AND LOWER(TRIM(COALESCE(provider_name, ''))) NOT IN ('unknown', 'pending')", ); push_sqlite_usage_provider_performance_filters( &mut timeline_builder, query, &mut has_where, ); push_sqlite_usage_where(&mut timeline_builder, &mut has_where); timeline_builder.push("provider_id IN ("); { let mut separated = timeline_builder.separated(", "); for provider_id in &provider_ids { separated.push_bind(provider_id.clone()); } } timeline_builder .push(") GROUP BY date, provider_id ORDER BY date ASC, provider_id ASC"); let rows = timeline_builder .build() .fetch_all(&self.pool) .await .map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageProviderPerformanceTimelineRow { date: row.try_get("date").map_sql_err()?, provider_id: row.try_get("provider_id").map_sql_err()?, provider: row.try_get("provider").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, success_count: sqlite_aggregate_u64(row, "success_count")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, avg_output_tps: sqlite_optional_real(row, "avg_output_tps")?, avg_first_byte_time_ms: sqlite_optional_real( row, "avg_first_byte_time_ms", )?, avg_response_time_ms: sqlite_optional_real(row, "avg_response_time_ms")?, slow_request_count: sqlite_aggregate_u64(row, "slow_request_count")?, }) }) .collect::, DataLayerError>>()? }; Ok(StoredUsageProviderPerformance { summary, providers, timeline, }) } async fn summarize_usage_cost_savings( &self, query: &UsageCostSavingsSummaryQuery, ) -> Result { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(StoredUsageCostSavingsSummary::default()); } let mut builder = QueryBuilder::::new(format!( r#" SELECT COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM(COALESCE(CAST(cache_read_cost_usd AS REAL), 0)), 0) AS cache_read_cost_usd, COALESCE(SUM(COALESCE(CAST(cache_creation_cost_usd AS REAL), 0)), 0) AS cache_creation_cost_usd, COALESCE(SUM( {input_price_expr} * MAX(COALESCE(cache_read_input_tokens, 0), 0) / 1000000.0 ), 0) AS estimated_full_cost_usd FROM "usage" "#, input_price_expr = sqlite_usage_metadata_input_price_expr() )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "provider_name", query.provider_name.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "model", query.model.as_deref(), ); let row = builder.build().fetch_one(&self.pool).await.map_sql_err()?; Ok(StoredUsageCostSavingsSummary { cache_read_tokens: sqlite_aggregate_u64(&row, "cache_read_tokens")?, cache_read_cost_usd: sqlite_real(&row, "cache_read_cost_usd")?, cache_creation_cost_usd: sqlite_real(&row, "cache_creation_cost_usd")?, estimated_full_cost_usd: sqlite_real(&row, "estimated_full_cost_usd")?, }) } async fn summarize_usage_time_series( &self, query: &UsageTimeSeriesQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let bucket_expr = sqlite_usage_bucket_expr(query.granularity, query.tz_offset_minutes); let mut builder = QueryBuilder::::new(format!( r#" SELECT {bucket_expr} AS bucket_key, COUNT(*) AS total_requests, COALESCE(SUM(MAX(COALESCE(input_tokens, 0), 0)), 0) AS input_tokens, COALESCE(SUM(MAX(COALESCE(output_tokens, 0), 0)), 0) AS output_tokens, COALESCE(SUM(MAX(COALESCE(cache_creation_input_tokens, 0), 0)), 0) AS cache_creation_tokens, COALESCE(SUM(MAX(COALESCE(cache_read_input_tokens, 0), 0)), 0) AS cache_read_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, COALESCE(SUM(MAX(COALESCE(response_time_ms, 0), 0)), 0) AS total_response_time_ms FROM "usage" "# )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "provider_name", query.provider_name.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "model", query.model.as_deref(), ); builder.push(" GROUP BY bucket_key ORDER BY bucket_key ASC"); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageTimeSeriesBucket { bucket_key: row.try_get("bucket_key").map_sql_err()?, total_requests: sqlite_aggregate_u64(row, "total_requests")?, input_tokens: sqlite_aggregate_u64(row, "input_tokens")?, output_tokens: sqlite_aggregate_u64(row, "output_tokens")?, cache_creation_tokens: sqlite_aggregate_u64(row, "cache_creation_tokens")?, cache_read_tokens: sqlite_aggregate_u64(row, "cache_read_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, total_response_time_ms: sqlite_real(row, "total_response_time_ms")?, }) }) .collect() } async fn summarize_usage_leaderboard( &self, query: &UsageLeaderboardQuery, ) -> Result, DataLayerError> { if query.created_from_unix_secs >= query.created_until_unix_secs { return Ok(Vec::new()); } let (group_key_expr, legacy_name_expr, extra_filter) = sqlite_usage_leaderboard_group_expr(query.group_by); let mut builder = QueryBuilder::::new(format!( r#" SELECT {group_key_expr} AS group_key, MAX({legacy_name_expr}) AS legacy_name, COUNT(*) AS request_count, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); let mut has_where = false; push_sqlite_usage_range( &mut builder, &mut has_where, query.created_from_unix_secs, query.created_until_unix_secs, ); push_sqlite_usage_finalized_filter(&mut builder, &mut has_where); push_sqlite_usage_where(&mut builder, &mut has_where); builder.push(extra_filter); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "user_id", query.user_id.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "provider_name", query.provider_name.as_deref(), ); push_sqlite_usage_optional_text_filter( &mut builder, &mut has_where, "model", query.model.as_deref(), ); builder.push(" GROUP BY group_key ORDER BY group_key ASC"); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; rows.iter() .map(|row| { Ok(StoredUsageLeaderboardSummary { group_key: row.try_get("group_key").map_sql_err()?, legacy_name: row.try_get("legacy_name").map_sql_err()?, request_count: sqlite_aggregate_u64(row, "request_count")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, }) }) .collect() } async fn list_recent_usage_audits( &self, user_id: Option<&str>, limit: usize, ) -> Result, DataLayerError> { if limit == 0 { return Ok(Vec::new()); } let mut builder = QueryBuilder::::new(USAGE_COLUMNS); let mut has_where = false; push_sqlite_usage_optional_text_filter(&mut builder, &mut has_where, "user_id", user_id); builder.push(" ORDER BY created_at_unix_ms DESC, id ASC LIMIT "); builder.push_bind(limit as i64); self.fetch_usage_items(builder).await } async fn summarize_total_tokens_by_api_key_ids( &self, api_key_ids: &[String], ) -> Result, DataLayerError> { if api_key_ids.is_empty() { return Ok(BTreeMap::new()); } let mut builder = QueryBuilder::::new(format!( r#" SELECT "usage".api_key_id, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".api_key_id IN ( "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); { let mut separated = builder.separated(", "); for api_key_id in api_key_ids { separated.push_bind(api_key_id.clone()); } } builder.push(") GROUP BY \"usage\".api_key_id ORDER BY \"usage\".api_key_id ASC"); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; let mut totals = BTreeMap::new(); for row in rows { totals.insert( row.try_get("api_key_id").map_sql_err()?, sqlite_aggregate_u64(&row, "total_tokens")?, ); } Ok(totals) } async fn summarize_usage_by_provider_api_key_ids( &self, provider_api_key_ids: &[String], ) -> Result, DataLayerError> { if provider_api_key_ids.is_empty() { return Ok(BTreeMap::new()); } let mut builder = QueryBuilder::::new(format!( r#" SELECT "usage".provider_api_key_id, COUNT(*) AS request_count, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST("usage".total_cost_usd AS REAL), 0)), 0) AS total_cost_usd, MAX("usage".created_at_unix_ms) AS last_used_at_unix_secs FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".provider_api_key_id IN ( "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )); { let mut separated = builder.separated(", "); for provider_api_key_id in provider_api_key_ids { separated.push_bind(provider_api_key_id.clone()); } } builder.push( ") GROUP BY \"usage\".provider_api_key_id ORDER BY \"usage\".provider_api_key_id ASC", ); let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?; let mut summaries = BTreeMap::new(); for row in rows { let provider_api_key_id: String = row.try_get("provider_api_key_id").map_sql_err()?; summaries.insert( provider_api_key_id.clone(), StoredProviderApiKeyUsageSummary { provider_api_key_id, request_count: sqlite_aggregate_u64(&row, "request_count")?, total_tokens: sqlite_aggregate_u64(&row, "total_tokens")?, total_cost_usd: sqlite_real(&row, "total_cost_usd")?, last_used_at_unix_secs: row .try_get::, _>("last_used_at_unix_secs") .map_sql_err()? .map(|value| value.max(0) as u64), }, ); } Ok(summaries) } async fn summarize_usage_by_provider_api_key_windows( &self, requests: &[ProviderApiKeyWindowUsageRequest], ) -> Result, DataLayerError> { let mut summaries = Vec::with_capacity(requests.len()); for request in requests { let provider_api_key_id = request.provider_api_key_id.trim(); if provider_api_key_id.is_empty() { return Err(DataLayerError::InvalidInput( "provider api key window usage provider_api_key_id cannot be empty".to_string(), )); } let window_code = request.window_code.trim(); if window_code.is_empty() { return Err(DataLayerError::InvalidInput( "provider api key window usage window_code cannot be empty".to_string(), )); } if request.start_unix_secs >= request.end_unix_secs { return Err(DataLayerError::InvalidInput( "provider api key window usage range must be non-empty".to_string(), )); } let row = sqlx::query(&format!( r#" SELECT COUNT(*) AS request_count, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, COALESCE(SUM(COALESCE(CAST("usage".total_cost_usd AS REAL), 0)), 0) AS total_cost_usd FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".provider_api_key_id = ? AND "usage".created_at_unix_ms >= ? AND "usage".created_at_unix_ms < ? "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )) .bind(provider_api_key_id) .bind(request.start_unix_secs as i64) .bind(request.end_unix_secs as i64) .fetch_one(&self.pool) .await .map_sql_err()?; summaries.push(StoredProviderApiKeyWindowUsageSummary { provider_api_key_id: provider_api_key_id.to_string(), window_code: window_code.to_string(), request_count: sqlite_aggregate_u64(&row, "request_count")?, total_tokens: sqlite_aggregate_u64(&row, "total_tokens")?, total_cost_usd: sqlite_real(&row, "total_cost_usd")?, }); } Ok(summaries) } async fn summarize_provider_usage_since( &self, provider_id: &str, since_unix_secs: u64, ) -> Result { let row = sqlx::query( r#" SELECT COUNT(*) AS total_requests, COALESCE(SUM(CASE WHEN LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) THEN 1 ELSE 0 END), 0) AS successful_requests, COALESCE(SUM(CASE WHEN status NOT IN ('pending', 'streaming') AND NOT ( LOWER(COALESCE(status, '')) IN ('completed', 'success', 'ok', 'billed', 'settled') AND (status_code IS NULL OR status_code < 400) ) THEN 1 ELSE 0 END), 0) AS failed_requests, COALESCE(AVG(CASE WHEN response_time_ms IS NOT NULL THEN MAX(COALESCE(response_time_ms, 0), 0) ELSE NULL END), 0) AS avg_response_time_ms, COALESCE(SUM(COALESCE(CAST(total_cost_usd AS REAL), 0)), 0) AS total_cost_usd FROM "usage" WHERE provider_id = ? AND created_at_unix_ms >= ? "#, ) .bind(provider_id) .bind(since_unix_secs as i64) .fetch_one(&self.pool) .await .map_sql_err()?; Ok(StoredProviderUsageSummary { total_requests: sqlite_aggregate_u64(&row, "total_requests")?, successful_requests: sqlite_aggregate_u64(&row, "successful_requests")?, failed_requests: sqlite_aggregate_u64(&row, "failed_requests")?, avg_response_time_ms: sqlite_real(&row, "avg_response_time_ms")?, total_cost_usd: sqlite_real(&row, "total_cost_usd")?, }) } async fn summarize_usage_daily_heatmap( &self, query: &UsageDailyHeatmapQuery, ) -> Result, DataLayerError> { let created_until_unix_secs = usage_current_unix_secs().saturating_add(1); let user_id = query.user_id.as_deref(); let mut summaries = BTreeMap::::new(); for item in self .summarize_usage_daily_heatmap_from_daily_aggregates( query.created_from_unix_secs, created_until_unix_secs, user_id, ) .await? { summaries.insert(item.date.clone(), item); } for item in self .summarize_usage_daily_heatmap_raw_from_range( query.created_from_unix_secs, created_until_unix_secs, user_id, ) .await? { summaries.entry(item.date.clone()).or_insert(item); } Ok(summaries.into_values().collect()) } async fn read_usage_counter_health( &self, ) -> Result { counters::read_health(&self.pool).await } async fn read_usage_counter_pending_health( &self, ) -> Result< aether_data_contracts::repository::usage::UsageCounterPendingHealthSnapshot, DataLayerError, > { counters::read_pending_health(&self.pool).await } } fn map_sqlite_usage_daily_summary( row: &SqliteRow, ) -> Result { Ok(StoredUsageDailySummary { date: row.try_get("date").map_sql_err()?, requests: sqlite_aggregate_u64(row, "requests")?, total_tokens: sqlite_aggregate_u64(row, "total_tokens")?, total_cost_usd: sqlite_real(row, "total_cost_usd")?, actual_total_cost_usd: sqlite_real(row, "actual_total_cost_usd")?, }) } fn usage_current_unix_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or_default() } fn first_byte_transition_allowed(existing: &StoredRequestUsageAudit) -> bool { existing.billing_status == "pending" && matches!(existing.status.as_str(), "pending" | "streaming") && existing.finalized_at_unix_secs.is_none() } impl SqliteUsageWriteRepository { pub fn new(pool: SqlitePool) -> Self { Self { pool } } pub async fn find_by_request_id( &self, request_id: &str, ) -> Result, DataLayerError> { let row = sqlx::query(&format!( "{USAGE_COLUMNS} WHERE \"usage\".request_id = ? LIMIT 1" )) .bind(request_id) .fetch_optional(&self.pool) .await .map_sql_err()?; let usage = row .as_ref() .map(|row| map_usage_row(row, true)) .transpose()?; match usage { Some(usage) => http_capture::hydrate_usage_body_refs(&self.pool, usage) .await .map(Some), None => Ok(None), } } async fn upsert_in_tx( tx: &mut sqlx::Transaction<'_, Sqlite>, usage: UpsertUsageRecord, ) -> Result<(), DataLayerError> { let mut usage = strip_deprecated_usage_display_fields(usage); usage.validate()?; let prepared_capture = http_capture::prepare_usage_http_capture(&mut usage)?; let existing = counters::lock_and_load_usage(tx, &usage.request_id).await?; let recovers_terminal_failure = existing.as_ref().is_some_and(|existing| { usage_can_recover_terminal_failure( &existing.status, &existing.billing_status, &usage.status, &usage.billing_status, ) }); if existing.as_ref().is_some_and(|existing| { (existing.billing_status == "settled" || existing.billing_status == "void") && !recovers_terminal_failure }) { return Ok(()); } let capture_update_allowed = recovers_terminal_failure || http_capture::capture_update_allowed(existing.as_ref(), &usage.status); if capture_update_allowed { http_capture::apply_previous_metadata_tombstones(&mut usage, existing.as_ref()); } let prepared_snapshots = capture_update_allowed .then(|| snapshots::from_usage(&usage)) .transpose()?; bind_upsert(sqlx::query(UPSERT_USAGE_SQL), &usage)? .execute(&mut **tx) .await .map_sql_err()?; if capture_update_allowed { http_capture::sync_usage_http_capture(tx, &usage.request_id, &prepared_capture).await?; let (routing_snapshot, settlement_snapshot) = prepared_snapshots .as_ref() .expect("capture-allowed usage has prepared snapshots"); snapshots::sync( tx, &usage.request_id, routing_snapshot, settlement_snapshot, matches!(usage.status.as_str(), "completed" | "failed" | "cancelled"), ) .await?; } counters::enqueue_usage_transition_for_request(tx, &usage.request_id, existing.as_ref()) .await } async fn execute_first_byte_batch( tx: &mut sqlx::Transaction<'_, Sqlite>, rows: &[&PreparedFirstByteUsage], preserve_existing_format_conversion: bool, ) -> Result<(), DataLayerError> { if rows.is_empty() { return Ok(()); } let mut builder = QueryBuilder::::new(UPSERT_FIRST_BYTE_BATCH_PREFIX_SQL); builder.push_values(rows, |mut values, row| { values .push_bind(row.usage.request_id.clone()) .push_bind(row.usage.request_id.clone()) .push_bind(row.usage.user_id.clone()) .push_bind(row.usage.api_key_id.clone()) .push_bind(row.usage.provider_name.clone()) .push_bind(row.usage.model.clone()) .push_bind(row.usage.target_model.clone()) .push_bind(row.usage.provider_id.clone()) .push_bind(row.usage.provider_endpoint_id.clone()) .push_bind(row.usage.provider_api_key_id.clone()) .push_bind(row.usage.request_type.clone()) .push_bind(row.usage.api_format.clone()) .push_bind(row.usage.api_family.clone()) .push_bind(row.usage.endpoint_kind.clone()) .push_bind(row.usage.endpoint_api_format.clone()) .push_bind(row.usage.provider_api_family.clone()) .push_bind(row.usage.provider_endpoint_kind.clone()) .push_bind(row.usage.has_format_conversion.unwrap_or(false)) .push("1") .push_bind( row.usage .request_metadata .as_ref() .and_then(serde_json::Value::as_object) .and_then(|metadata| metadata.get(UPSTREAM_IS_STREAM_KEY)) .and_then(serde_json::Value::as_bool) .unwrap_or(true), ) .push_bind(row.usage.status_code.map(i64::from)) .push_bind(row.response_time_ms) .push_bind(row.first_byte_time_ms) .push("'streaming'") .push("'pending'") .push_bind(row.request_metadata_json.clone()) .push_bind(row.created_at_unix_secs) .push_bind(row.created_at_unix_secs) .push_bind(row.updated_at_unix_secs); }); builder.push(UPSERT_FIRST_BYTE_BATCH_UPDATE_PREFIX_SQL); if preserve_existing_format_conversion { builder.push("COALESCE(\"usage\".has_format_conversion, 0)"); } else { builder.push( "COALESCE(excluded.has_format_conversion, \"usage\".has_format_conversion, 0)", ); } builder.push(UPSERT_FIRST_BYTE_BATCH_UPDATE_SUFFIX_SQL); builder.build().execute(&mut **tx).await.map_sql_err()?; Ok(()) } async fn upsert_first_byte_many_native( &self, usages: Vec, ) -> Result<(), DataLayerError> { if usages.is_empty() { return Ok(()); } let mut request_id_counts = BTreeMap::::new(); for usage in &usages { *request_id_counts .entry(usage.request_id.clone()) .or_default() += 1; } let mut batch_rows = Vec::new(); let mut fallback_rows = Vec::new(); for (sequence, usage) in usages.into_iter().enumerate() { let prepared = PreparedFirstByteUsage::try_from_usage(usage)?; if request_id_counts .get(&prepared.usage.request_id) .copied() .unwrap_or_default() == 1 { batch_rows.push(prepared); } else { fallback_rows.push((sequence, prepared)); } } let mut tx = self.pool.begin().await.map_sql_err()?; let mut before = BTreeMap::>::new(); let request_ids = batch_rows .iter() .map(|row| row.usage.request_id.clone()) .collect::>(); for request_id in request_ids { let existing = counters::lock_and_load_usage(&mut tx, &request_id).await?; before.insert(request_id, existing); } let eligible_rows = batch_rows .iter() .filter(|row| { before .get(&row.usage.request_id) .and_then(Option::as_ref) .is_none_or(first_byte_transition_allowed) }) .collect::>(); for preserve_existing_format_conversion in [true, false] { let matching = eligible_rows .iter() .copied() .filter(|row| { row.usage.has_format_conversion.is_none() == preserve_existing_format_conversion }) .collect::>(); for chunk in matching.chunks(SQLITE_FIRST_BYTE_BATCH_SIZE) { Self::execute_first_byte_batch(&mut tx, chunk, preserve_existing_format_conversion) .await?; } } for row in eligible_rows { counters::enqueue_usage_transition_for_request( &mut tx, &row.usage.request_id, before.get(&row.usage.request_id).and_then(Option::as_ref), ) .await?; } fallback_rows.sort_by_key(|(sequence, _)| *sequence); for (_, row) in fallback_rows { let existing = counters::lock_and_load_usage(&mut tx, &row.usage.request_id).await?; if existing .as_ref() .is_some_and(|existing| !first_byte_transition_allowed(existing)) { continue; } Self::execute_first_byte_batch( &mut tx, &[&row], row.usage.has_format_conversion.is_none(), ) .await?; counters::enqueue_usage_transition_for_request( &mut tx, &row.usage.request_id, existing.as_ref(), ) .await?; } tx.commit().await.map_sql_err() } } #[async_trait] impl UsageWriteRepository for SqliteUsageWriteRepository { fn supports_first_byte_usage_fast_path(&self) -> bool { true } fn supports_first_byte_usage_batch(&self) -> bool { true } fn supports_pending_usage_batch(&self) -> bool { true } async fn upsert( &self, usage: UpsertUsageRecord, ) -> Result { usage.validate()?; let request_id = usage.request_id.clone(); let mut tx = self.pool.begin().await.map_sql_err()?; Self::upsert_in_tx(&mut tx, usage).await?; tx.commit().await.map_sql_err()?; self.find_by_request_id(&request_id).await?.ok_or_else(|| { DataLayerError::UnexpectedValue("usage upsert returned no row".to_string()) }) } async fn upsert_first_byte(&self, usage: UpsertUsageRecord) -> Result<(), DataLayerError> { self.upsert_first_byte_many_native(vec![usage]).await } async fn upsert_first_byte_many( &self, usages: Vec, ) -> Result<(), DataLayerError> { self.upsert_first_byte_many_native(usages).await } async fn upsert_pending_many( &self, usages: Vec, ) -> Result<(), DataLayerError> { if usages.is_empty() { return Ok(()); } for usage in &usages { usage.validate()?; if usage.status != "pending" || usage.billing_status != "pending" { return Err(DataLayerError::InvalidInput( "pending usage batch requires pending status with pending billing".to_string(), )); } } let mut tx = self.pool.begin().await.map_sql_err()?; for usage in usages { Self::upsert_in_tx(&mut tx, usage).await?; } tx.commit().await.map_sql_err() } async fn rebuild_api_key_usage_stats(&self) -> Result { sqlx::query( r#" UPDATE api_keys SET total_requests = 0, total_tokens = 0, total_cost_usd = 0.0, last_used_at = NULL "#, ) .execute(&self.pool) .await .map_sql_err()?; let rows = sqlx::query(&format!( r#" SELECT "usage".api_key_id, COUNT(*) AS total_requests, COALESCE(SUM({total_tokens_expr}), 0) AS total_tokens, CAST(COALESCE(SUM("usage".total_cost_usd), 0) AS REAL) AS total_cost_usd, MAX(COALESCE( "usage".created_at, "usage".created_at_unix_ms, "usage".updated_at_unix_secs )) AS last_used_at FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".api_key_id IS NOT NULL AND TRIM("usage".api_key_id) <> '' AND "usage".status NOT IN ('pending', 'streaming') GROUP BY "usage".api_key_id "#, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )) .fetch_all(&self.pool) .await .map_sql_err()?; for row in &rows { sqlx::query( r#" UPDATE api_keys SET total_requests = ?, total_tokens = ?, total_cost_usd = ?, last_used_at = ? WHERE id = ? "#, ) .bind(row.try_get::("total_requests").map_sql_err()?) .bind(row.try_get::("total_tokens").map_sql_err()?) .bind(sqlite_real(row, "total_cost_usd")?) .bind( row.try_get::, _>("last_used_at") .map_sql_err()?, ) .bind(row.try_get::("api_key_id").map_sql_err()?) .execute(&self.pool) .await .map_sql_err()?; } Ok(rows.len() as u64) } async fn rebuild_provider_api_key_usage_stats(&self) -> Result { sqlx::query( r#" UPDATE provider_api_keys SET request_count = 0, success_count = 0, error_count = 0, total_tokens = 0, total_cost_usd = 0.0, total_response_time_ms = 0, last_used_at = NULL "#, ) .execute(&self.pool) .await .map_sql_err()?; let rows = sqlx::query(&format!( r#" SELECT "usage".provider_api_key_id, COUNT(*) AS request_count, COALESCE(SUM({success_flag_expr}), 0) AS success_count, COALESCE(SUM({error_flag_expr}), 0) AS error_count, COALESCE(SUM(CASE WHEN "usage".status IN ('pending', 'streaming') THEN 0 ELSE {total_tokens_expr} END), 0) AS total_tokens, COALESCE(SUM(CASE WHEN "usage".status IN ('pending', 'streaming') THEN 0 ELSE COALESCE(CAST("usage".total_cost_usd AS REAL), 0) END), 0) AS total_cost_usd, COALESCE(SUM(CASE WHEN {success_flag_expr} = 1 AND "usage".response_time_ms IS NOT NULL THEN MAX(COALESCE("usage".response_time_ms, 0), 0) ELSE 0 END), 0) AS total_response_time_ms, MAX("usage".created_at_unix_ms) AS last_used_at FROM "usage" LEFT JOIN usage_settlement_snapshots AS settlement ON settlement.request_id = "usage".request_id WHERE "usage".provider_api_key_id IS NOT NULL AND TRIM("usage".provider_api_key_id) <> '' GROUP BY "usage".provider_api_key_id "#, success_flag_expr = SQLITE_PROVIDER_KEY_SUCCESS_FLAG_EXPR, error_flag_expr = SQLITE_PROVIDER_KEY_ERROR_FLAG_EXPR, total_tokens_expr = SQLITE_USAGE_CANONICAL_TOTAL_TOKENS_EXPR )) .fetch_all(&self.pool) .await .map_sql_err()?; for row in &rows { sqlx::query( r#" UPDATE provider_api_keys SET request_count = ?, success_count = ?, error_count = ?, total_tokens = ?, total_cost_usd = ?, total_response_time_ms = ?, last_used_at = ? WHERE id = ? "#, ) .bind(row.try_get::("request_count").map_sql_err()?) .bind(row.try_get::("success_count").map_sql_err()?) .bind(row.try_get::("error_count").map_sql_err()?) .bind(row.try_get::("total_tokens").map_sql_err()?) .bind(sqlite_real(row, "total_cost_usd")?) .bind( row.try_get::("total_response_time_ms") .map_sql_err()?, ) .bind( row.try_get::, _>("last_used_at") .map_sql_err()?, ) .bind( row.try_get::("provider_api_key_id") .map_sql_err()?, ) .execute(&self.pool) .await .map_sql_err()?; } Ok(rows.len() as u64) } async fn cleanup_stale_pending_requests( &self, cutoff_unix_secs: u64, now_unix_secs: u64, timeout_minutes: u64, batch_size: usize, ) -> Result { if batch_size == 0 { return Ok(PendingUsageCleanupSummary::default()); } let now_unix_ms = now_unix_secs.saturating_mul(1000); let mut summary = PendingUsageCleanupSummary::default(); let batch_size_u64 = u64::try_from(batch_size).map_err(|_| { DataLayerError::InvalidInput(format!( "invalid stale pending usage batch size: {batch_size}" )) })?; loop { let mut tx = self.pool.begin().await.map_sql_err()?; let stale_rows = sqlx::query(SELECT_STALE_PENDING_USAGE_BATCH_SQL) .bind(to_i64(cutoff_unix_secs, "stale pending usage cutoff")?) .bind(to_i64(batch_size_u64, "stale pending usage batch size")?) .fetch_all(&mut *tx) .await .map_sql_err()?; if stale_rows.is_empty() { tx.rollback().await.map_sql_err()?; break; } let stale_rows = stale_rows .iter() .map(|row| { Ok(StalePendingUsageRow { request_id: row.try_get("request_id").map_sql_err()?, status: row.try_get("status").map_sql_err()?, billing_status: row.try_get("billing_status").map_sql_err()?, }) }) .collect::, DataLayerError>>()?; let completed_request_ids = completed_request_ids_sqlite(&mut tx, stale_rows.iter().map(|row| &row.request_id)) .await?; for row in stale_rows { if completed_request_ids.contains(&row.request_id) { sqlx::query( r#" UPDATE "usage" SET status = 'completed', status_code = 200, error_message = NULL WHERE request_id = ? "#, ) .bind(&row.request_id) .execute(&mut *tx) .await .map_sql_err()?; sqlx::query( r#" UPDATE request_candidates SET status = 'success', finished_at = ? WHERE request_id = ? AND status = 'streaming' "#, ) .bind(to_i64(now_unix_ms, "request candidate finished_at")?) .bind(&row.request_id) .execute(&mut *tx) .await .map_sql_err()?; summary.recovered += 1; continue; } let candidate_info = latest_failed_candidate_sqlite(&mut tx, &row.request_id).await?; let (status_code, error_message) = resolve_stale_pending_failure( candidate_info.as_ref(), &row.status, timeout_minutes, ); let status_code_i64 = i64::from(status_code); if row.billing_status == "pending" { sqlx::query( r#" UPDATE "usage" SET status = 'failed', status_code = ?, error_message = ?, billing_status = 'void', finalized_at = ?, total_cost_usd = 0.0, actual_total_cost_usd = 0.0 WHERE request_id = ? "#, ) .bind(status_code_i64) .bind(&error_message) .bind(to_i64(now_unix_secs, "usage finalized_at")?) .bind(&row.request_id) .execute(&mut *tx) .await .map_sql_err()?; upsert_void_usage_settlement_snapshot_sqlite( &mut tx, &row.request_id, now_unix_secs, ) .await?; } else { sqlx::query( r#" UPDATE "usage" SET status = 'failed', status_code = ?, error_message = ? WHERE request_id = ? "#, ) .bind(status_code_i64) .bind(&error_message) .bind(&row.request_id) .execute(&mut *tx) .await .map_sql_err()?; } sqlx::query( r#" UPDATE request_candidates SET status = 'failed', finished_at = ?, error_message = '请求超时(服务器可能已重启)' WHERE request_id = ? AND status IN ('pending', 'streaming') "#, ) .bind(to_i64(now_unix_ms, "request candidate finished_at")?) .bind(&row.request_id) .execute(&mut *tx) .await .map_sql_err()?; summary.failed += 1; } tx.commit().await.map_sql_err()?; } Ok(summary) } async fn flush_usage_counter_deltas( &self, batch_size: usize, ) -> Result { counters::flush(&self.pool, batch_size).await } async fn enqueue_proxy_node_counter_delta( &self, delta: aether_data_contracts::repository::usage::ProxyNodeCounterDelta, ) -> Result { counters::enqueue_proxy_node(&self.pool, delta).await } async fn enqueue_management_token_counter_delta( &self, delta: aether_data_contracts::repository::usage::ManagementTokenCounterDelta, ) -> Result { counters::enqueue_management_token(&self.pool, delta).await } async fn enqueue_api_key_last_used_delta( &self, delta: aether_data_contracts::repository::usage::ApiKeyLastUsedDelta, ) -> Result { counters::enqueue_api_key_last_used(&self.pool, delta).await } async fn cleanup_processed_usage_counter_deltas( &self, cutoff_unix_secs: u64, batch_size: usize, ) -> Result { counters::cleanup_processed(&self.pool, cutoff_unix_secs, batch_size).await } async fn cleanup_usage( &self, window: &UsageCleanupWindow, batch_size: usize, auto_delete_expired_keys: bool, targets: UsageCleanupTargets, mode: UsageCleanupExecutionMode, ) -> Result { cleanup::cleanup_usage( &self.pool, window, batch_size, auto_delete_expired_keys, targets, mode, ) .await } async fn preview_usage_cleanup( &self, window: &UsageCleanupWindow, targets: UsageCleanupTargets, mode: UsageCleanupExecutionMode, ) -> Result { cleanup::preview_usage_cleanup(&self.pool, window, targets, mode).await } } struct StalePendingUsageRow { request_id: String, status: String, billing_status: String, } async fn completed_request_ids_sqlite<'a>( tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, request_ids: impl Iterator, ) -> Result, DataLayerError> { let mut completed = HashSet::new(); for request_id in request_ids { let rows = sqlx::query(SELECT_COMPLETED_REQUEST_CANDIDATES_SQL) .bind(request_id) .fetch_all(&mut **tx) .await .map_sql_err()?; let mut is_completed = false; for row in &rows { if candidate_row_is_completed(row)? { is_completed = true; break; } } if is_completed { completed.insert(request_id.clone()); } } Ok(completed) } fn candidate_row_is_completed(row: &SqliteRow) -> Result { let status: String = row.try_get("status").map_sql_err()?; if status == "streaming" { return Ok(true); } if status != "success" { return Ok(false); } let Some(extra_data) = row .try_get::, _>("extra_data") .map_sql_err()? else { return Ok(false); }; let Ok(value) = serde_json::from_str::(&extra_data) else { return Ok(false); }; Ok(value .get("stream_completed") .and_then(serde_json::Value::as_bool) .unwrap_or(false)) } async fn upsert_void_usage_settlement_snapshot_sqlite( tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, request_id: &str, now_unix_secs: u64, ) -> Result<(), DataLayerError> { let now = to_i64(now_unix_secs, "usage settlement snapshot timestamp")?; sqlx::query( r#" INSERT INTO usage_settlement_snapshots ( request_id, billing_status, finalized_at, created_at, updated_at ) VALUES (?, 'void', ?, ?, ?) ON CONFLICT (request_id) DO UPDATE SET billing_status = excluded.billing_status, billing_snapshot_schema_version = NULL, billing_snapshot_status = NULL, settlement_snapshot_schema_version = NULL, settlement_snapshot = NULL, billing_dimensions = NULL, billing_input_tokens = NULL, billing_effective_input_tokens = NULL, billing_output_tokens = NULL, billing_cache_creation_tokens = NULL, billing_cache_creation_5m_tokens = NULL, billing_cache_creation_1h_tokens = NULL, billing_cache_read_tokens = NULL, billing_total_input_context = NULL, billing_cache_creation_cost_usd = NULL, billing_cache_read_cost_usd = NULL, billing_total_cost_usd = NULL, billing_actual_total_cost_usd = NULL, billing_pricing_source = NULL, billing_rule_id = NULL, billing_rule_version = NULL, rate_multiplier = NULL, is_free_tier = NULL, input_price_per_1m = NULL, output_price_per_1m = NULL, cache_creation_price_per_1m = NULL, cache_read_price_per_1m = NULL, price_per_request = NULL, finalized_at = COALESCE(usage_settlement_snapshots.finalized_at, excluded.finalized_at), updated_at = excluded.updated_at "#, ) .bind(request_id) .bind(now) .bind(now) .bind(now) .execute(&mut **tx) .await .map_sql_err()?; Ok(()) } fn stale_pending_error_message(status: &str, timeout_minutes: u64) -> String { format!("请求超时: 状态 '{status}' 超过 {timeout_minutes} 分钟未完成") } struct FailedCandidateCleanupInfo { status_code: Option, error_message: Option, } fn resolve_stale_pending_failure( candidate: Option<&FailedCandidateCleanupInfo>, status: &str, timeout_minutes: u64, ) -> (u16, String) { match candidate { Some(info) => ( info.status_code.unwrap_or(502), info.error_message .clone() .unwrap_or_else(|| stale_pending_error_message(status, timeout_minutes)), ), None => (504, stale_pending_error_message(status, timeout_minutes)), } } async fn latest_failed_candidate_sqlite( tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, request_id: &str, ) -> Result, DataLayerError> { let row = sqlx::query( r#" SELECT status_code, error_message FROM request_candidates WHERE request_id = ? AND status IN ('failed', 'cancelled') ORDER BY COALESCE(finished_at, started_at, created_at) DESC, retry_index DESC, candidate_index DESC LIMIT 1 "#, ) .bind(request_id) .fetch_optional(&mut **tx) .await .map_sql_err()?; let Some(row) = row else { return Ok(None); }; let status_code = row .try_get::, _>("status_code") .map_sql_err()? .and_then(|value| u16::try_from(value).ok()); let error_message = row .try_get::, _>("error_message") .map_sql_err()? .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); Ok(Some(FailedCandidateCleanupInfo { status_code, error_message, })) } fn bind_upsert<'q>( mut query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>, usage: &'q UpsertUsageRecord, ) -> Result>, DataLayerError> { let input_tokens = usage.input_tokens.unwrap_or_default(); let output_tokens = usage.output_tokens.unwrap_or_default(); let cache_creation_tokens = usage .cache_creation_input_tokens .or_else(|| { Some( usage .cache_creation_ephemeral_5m_input_tokens .unwrap_or_default() + usage .cache_creation_ephemeral_1h_input_tokens .unwrap_or_default(), ) }) .unwrap_or_default(); let cache_read_tokens = usage.cache_read_input_tokens.unwrap_or_default(); let total_tokens = usage .total_tokens .unwrap_or(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens); let created_at = usage .created_at_unix_ms .unwrap_or(usage.updated_at_unix_secs); let request_metadata = usage .request_metadata .as_ref() .map(serde_json::to_string) .transpose() .map_err(|err| DataLayerError::InvalidInput(err.to_string()))?; query = query .bind(&usage.request_id) .bind(&usage.request_id) .bind(usage.user_id.as_deref()) .bind(usage.api_key_id.as_deref()) .bind(&usage.provider_name) .bind(&usage.model) .bind(usage.target_model.as_deref()) .bind(usage.provider_id.as_deref()) .bind(usage.provider_endpoint_id.as_deref()) .bind(usage.provider_api_key_id.as_deref()) .bind(usage.request_type.as_deref()) .bind(usage.api_format.as_deref()) .bind(usage.api_family.as_deref()) .bind(usage.endpoint_kind.as_deref()) .bind(usage.endpoint_api_format.as_deref()) .bind(usage.provider_api_family.as_deref()) .bind(usage.provider_endpoint_kind.as_deref()) .bind(i64::from(usage.has_format_conversion.unwrap_or(false))) .bind(i64::from(usage.is_stream.unwrap_or(false))) .bind(i64::from(usage_upstream_is_stream(usage))) .bind(to_i64(input_tokens, "input_tokens")?) .bind(to_i64(output_tokens, "output_tokens")?) .bind(to_i64(total_tokens, "total_tokens")?) .bind(to_i64( cache_creation_tokens, "cache_creation_input_tokens", )?) .bind(to_i64( usage .cache_creation_ephemeral_5m_input_tokens .unwrap_or_default(), "cache_creation_ephemeral_5m_input_tokens", )?) .bind(to_i64( usage .cache_creation_ephemeral_1h_input_tokens .unwrap_or_default(), "cache_creation_ephemeral_1h_input_tokens", )?) .bind(to_i64(cache_read_tokens, "cache_read_input_tokens")?) .bind(usage.cache_creation_cost_usd.unwrap_or_default()) .bind(usage.cache_read_cost_usd.unwrap_or_default()) .bind(usage.output_price_per_1m) .bind(usage.total_cost_usd.unwrap_or_default()) .bind(usage.actual_total_cost_usd.unwrap_or_default()) .bind(usage.status_code.map(i64::from)) .bind(usage.error_message.as_deref()) .bind(usage.error_category.as_deref()) .bind(usage.response_time_ms.map(|value| value as i64)) .bind(usage.first_byte_time_ms.map(|value| value as i64)) .bind(&usage.status) .bind(&usage.billing_status) .bind(request_metadata) .bind(usage.candidate_id.as_deref()) .bind(usage.candidate_index.map(|value| value as i64)) .bind(usage.key_name.as_deref()) .bind(usage.planner_kind.as_deref()) .bind(usage.route_family.as_deref()) .bind(usage.route_kind.as_deref()) .bind(usage.execution_path.as_deref()) .bind(usage.local_execution_runtime_miss_reason.as_deref()) .bind(usage.finalized_at_unix_secs.map(|value| value as i64)) .bind(to_i64(created_at, "created_at_unix_ms")?) .bind(to_i64(usage.updated_at_unix_secs, "updated_at_unix_secs")?); Ok(query) } fn map_usage_row( row: &SqliteRow, resolve_legacy_compressed: bool, ) -> Result { let mut audit = StoredRequestUsageAudit::new( row.try_get("id").map_sql_err()?, row.try_get("request_id").map_sql_err()?, row.try_get("user_id").map_sql_err()?, row.try_get("api_key_id").map_sql_err()?, row.try_get("username").map_sql_err()?, row.try_get("api_key_name").map_sql_err()?, row.try_get("provider_name").map_sql_err()?, row.try_get("model").map_sql_err()?, row.try_get("target_model").map_sql_err()?, row.try_get("provider_id").map_sql_err()?, row.try_get("provider_endpoint_id").map_sql_err()?, row.try_get("provider_api_key_id").map_sql_err()?, row.try_get("request_type").map_sql_err()?, row.try_get("api_format").map_sql_err()?, row.try_get("api_family").map_sql_err()?, row.try_get("endpoint_kind").map_sql_err()?, row.try_get("endpoint_api_format").map_sql_err()?, row.try_get("provider_api_family").map_sql_err()?, row.try_get("provider_endpoint_kind").map_sql_err()?, row.try_get::("has_format_conversion") .map_sql_err()? != 0, row.try_get::("is_stream").map_sql_err()? != 0, row_i32(row, "input_tokens")?, row_i32(row, "output_tokens")?, row_i32(row, "total_tokens")?, sqlite_real(row, "total_cost_usd")?, sqlite_real(row, "actual_total_cost_usd")?, row_optional_i32(row, "status_code")?, row.try_get("error_message").map_sql_err()?, row.try_get("error_category").map_sql_err()?, row_optional_i32(row, "response_time_ms")?, row_optional_i32(row, "first_byte_time_ms")?, row.try_get("status").map_sql_err()?, row.try_get("billing_status").map_sql_err()?, row.try_get("created_at_unix_ms").map_sql_err()?, row.try_get("updated_at_unix_secs").map_sql_err()?, row.try_get("finalized_at_unix_secs").map_sql_err()?, )?; audit.cache_creation_input_tokens = row_u64(row, "cache_creation_input_tokens")?; audit.cache_creation_ephemeral_5m_input_tokens = row_u64(row, "cache_creation_ephemeral_5m_input_tokens")?; audit.cache_creation_ephemeral_1h_input_tokens = row_u64(row, "cache_creation_ephemeral_1h_input_tokens")?; audit.cache_read_input_tokens = row_u64(row, "cache_read_input_tokens")?; audit.cache_creation_cost_usd = sqlite_optional_real(row, "cache_creation_cost_usd")?.unwrap_or(0.0); audit.cache_read_cost_usd = sqlite_optional_real(row, "cache_read_cost_usd")?.unwrap_or(0.0); audit.output_price_per_1m = sqlite_optional_real(row, "output_price_per_1m")?; audit.request_metadata = row .try_get::, _>("request_metadata") .map_sql_err()? .map(|raw| serde_json::from_str(&raw)) .transpose() .map_err(|err| DataLayerError::UnexpectedValue(err.to_string()))?; audit.client_family = usage_request_metadata_client_family(audit.request_metadata.as_ref()) .map(ToOwned::to_owned); http_capture::hydrate_usage_row(row, &mut audit, resolve_legacy_compressed)?; let upstream_is_stream = row .try_get::, _>("upstream_is_stream") .map_sql_err()? .map(|value| value != 0); merge_usage_stream_metadata(&mut audit.request_metadata, upstream_is_stream); snapshots::hydrate_row(row, &mut audit)?; Ok(audit) } fn to_i64(value: u64, field: &str) -> Result { i64::try_from(value).map_err(|_| DataLayerError::InvalidInput(format!("{field} overflow"))) } fn usage_upstream_is_stream(usage: &UpsertUsageRecord) -> bool { usage .request_metadata .as_ref() .and_then(serde_json::Value::as_object) .and_then(|metadata| metadata.get(UPSTREAM_IS_STREAM_KEY)) .and_then(serde_json::Value::as_bool) .unwrap_or_else(|| usage.is_stream.unwrap_or(false)) } fn merge_usage_stream_metadata(metadata: &mut Option, upstream: Option) { let Some(upstream) = upstream else { return; }; let value = metadata.get_or_insert_with(|| serde_json::json!({})); let Some(object) = value.as_object_mut() else { return; }; object .entry(UPSTREAM_IS_STREAM_KEY) .or_insert(serde_json::Value::Bool(upstream)); } fn row_i32(row: &SqliteRow, field: &str) -> Result { let value: i64 = row.try_get(field).map_sql_err()?; i32::try_from(value).map_err(|_| DataLayerError::UnexpectedValue(format!("{field} overflow"))) } fn row_optional_i32(row: &SqliteRow, field: &str) -> Result, DataLayerError> { row.try_get::, _>(field) .map_sql_err()? .map(|value| { i32::try_from(value) .map_err(|_| DataLayerError::UnexpectedValue(format!("{field} overflow"))) }) .transpose() } fn row_u64(row: &SqliteRow, field: &str) -> Result { let value: i64 = row.try_get(field).map_sql_err()?; u64::try_from(value).map_err(|_| DataLayerError::UnexpectedValue(format!("{field} negative"))) } #[cfg(test)] mod tests;