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