systemprompt_analytics/repository/costs/
platform.rs1use super::CostAnalyticsRepository;
16use crate::Result;
17use chrono::{DateTime, Utc};
18use systemprompt_identifiers::UserId;
19
20use crate::models::reporting::{
21 CostBreakdownRow, CostSummaryRow, CostTrendRow, CostUserBreakdownRow, PreviousCostRow,
22};
23
24impl CostAnalyticsRepository {
25 pub async fn get_summary(
26 &self,
27 start: DateTime<Utc>,
28 end: DateTime<Utc>,
29 ) -> Result<CostSummaryRow> {
30 sqlx::query_as!(
31 CostSummaryRow,
32 r#"
33 SELECT
34 COALESCE(SUM(request_count), 0)::bigint as "requests!",
35 SUM(cost_microdollars)::bigint as "cost",
36 SUM(tokens)::bigint as "tokens",
37 SUM(reasoning_tokens)::bigint as "reasoning_tokens",
38 SUM(cache_read_tokens)::bigint as "cache_read_tokens",
39 SUM(cache_creation_tokens)::bigint as "cache_creation_tokens"
40 FROM report_ai_usage_hourly
41 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
42 AND NOT synthetic
43 "#,
44 start,
45 end
46 )
47 .fetch_one(&*self.pool)
48 .await
49 .map_err(Into::into)
50 }
51
52 pub async fn get_previous_cost(
53 &self,
54 start: DateTime<Utc>,
55 end: DateTime<Utc>,
56 ) -> Result<PreviousCostRow> {
57 sqlx::query_as!(
58 PreviousCostRow,
59 r#"
60 SELECT SUM(cost_microdollars)::bigint as "cost"
61 FROM report_ai_usage_hourly
62 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
63 AND NOT synthetic
64 "#,
65 start,
66 end
67 )
68 .fetch_one(&*self.pool)
69 .await
70 .map_err(Into::into)
71 }
72
73 pub async fn get_breakdown_by_model(
74 &self,
75 start: DateTime<Utc>,
76 end: DateTime<Utc>,
77 limit: i64,
78 ) -> Result<Vec<CostBreakdownRow>> {
79 sqlx::query_as!(
80 CostBreakdownRow,
81 r#"
82 SELECT
83 model as "name!",
84 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
85 SUM(request_count)::bigint as "requests!",
86 COALESCE(SUM(tokens), 0)::bigint as "tokens!"
87 FROM report_ai_usage_hourly
88 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
89 AND NOT synthetic
90 AND model <> ''
91 GROUP BY model
92 ORDER BY SUM(cost_microdollars) DESC NULLS LAST
93 LIMIT $3
94 "#,
95 start,
96 end,
97 limit
98 )
99 .fetch_all(&*self.pool)
100 .await
101 .map_err(Into::into)
102 }
103
104 pub async fn get_breakdown_by_provider(
105 &self,
106 start: DateTime<Utc>,
107 end: DateTime<Utc>,
108 limit: i64,
109 ) -> Result<Vec<CostBreakdownRow>> {
110 sqlx::query_as!(
111 CostBreakdownRow,
112 r#"
113 SELECT
114 provider as "name!",
115 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
116 SUM(request_count)::bigint as "requests!",
117 COALESCE(SUM(tokens), 0)::bigint as "tokens!"
118 FROM report_ai_usage_hourly
119 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
120 AND NOT synthetic
121 AND provider <> ''
122 GROUP BY provider
123 ORDER BY SUM(cost_microdollars) DESC NULLS LAST
124 LIMIT $3
125 "#,
126 start,
127 end,
128 limit
129 )
130 .fetch_all(&*self.pool)
131 .await
132 .map_err(Into::into)
133 }
134
135 pub async fn get_breakdown_by_user(
136 &self,
137 start: DateTime<Utc>,
138 end: DateTime<Utc>,
139 limit: i64,
140 ) -> Result<Vec<CostUserBreakdownRow>> {
141 sqlx::query_as!(
142 CostUserBreakdownRow,
143 r#"
144 SELECT
145 r.user_id as "user_id!: UserId",
146 u.name as "name?",
147 COALESCE(SUM(r.cost_microdollars), 0)::bigint as "cost!",
148 COUNT(*)::bigint as "requests!",
149 COALESCE(SUM(r.tokens_used), 0)::bigint as "tokens!",
150 COUNT(DISTINCT r.context_id)::bigint as "conversations!"
151 FROM report_ai_requests r
152 LEFT JOIN report_users u ON u.id = r.user_id
153 WHERE r.created_at >= $1 AND r.created_at < $2
154 AND NOT r.synthetic
155 GROUP BY r.user_id, u.name
156 ORDER BY SUM(r.cost_microdollars) DESC NULLS LAST
157 LIMIT $3
158 "#,
159 start,
160 end,
161 limit
162 )
163 .fetch_all(&*self.pool)
164 .await
165 .map_err(Into::into)
166 }
167
168 pub async fn get_breakdown_by_agent(
169 &self,
170 start: DateTime<Utc>,
171 end: DateTime<Utc>,
172 limit: i64,
173 ) -> Result<Vec<CostBreakdownRow>> {
174 sqlx::query_as!(
175 CostBreakdownRow,
176 r#"
177 (
178 SELECT
179 agent_name as "name!",
180 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
181 SUM(request_count)::bigint as "requests!",
182 COALESCE(SUM(tokens), 0)::bigint as "tokens!"
183 FROM report_ai_usage_hourly
184 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
185 AND NOT synthetic
186 AND agent_name <> ''
187 GROUP BY agent_name
188 ORDER BY SUM(cost_microdollars) DESC NULLS LAST
189 LIMIT $3
190 )
191 UNION ALL
192 (
193 SELECT
194 'unattributed' as "name!",
195 COALESCE(SUM(cost_microdollars), 0)::bigint as "cost!",
196 COALESCE(SUM(request_count), 0)::bigint as "requests!",
197 COALESCE(SUM(tokens), 0)::bigint as "tokens!"
198 FROM report_ai_usage_hourly
199 WHERE bucket >= date_trunc('hour', $1::timestamptz, 'UTC') AND bucket < $2
200 AND NOT synthetic
201 AND agent_name = ''
202 HAVING SUM(request_count) > 0
203 )
204 "#,
205 start,
206 end,
207 limit
208 )
209 .fetch_all(&*self.pool)
210 .await
211 .map_err(Into::into)
212 }
213
214 pub async fn get_costs_for_trends(
215 &self,
216 start: DateTime<Utc>,
217 end: DateTime<Utc>,
218 ) -> Result<Vec<CostTrendRow>> {
219 sqlx::query_as!(
220 CostTrendRow,
221 r#"
222 SELECT
223 created_at as "created_at!",
224 cost_microdollars,
225 tokens_used
226 FROM report_ai_requests
227 WHERE created_at >= $1 AND created_at < $2
228 AND NOT synthetic
229 ORDER BY created_at
230 "#,
231 start,
232 end
233 )
234 .fetch_all(&*self.pool)
235 .await
236 .map_err(Into::into)
237 }
238}