1use serde::Deserialize;
2use sqlx::SqlitePool;
3
4#[derive(Deserialize, Debug)]
5pub struct Model {
6 pub id: String,
7 #[serde(rename = "providerID")]
8 pub provider_id: String,
9 pub variant: Option<String>,
10}
11
12#[derive(Debug, sqlx::FromRow)]
13pub struct RowUsageSession {
14 pub id: String,
15 pub project_id: String,
16 pub slug: String,
17 pub title: String,
18 pub cost: f64,
19 pub tokens_input: i64,
20 pub tokens_output: i64,
21 pub tokens_reasoning: i64,
22 pub tokens_cache_read: i64,
23 pub tokens_cache_write: i64,
24 pub model: String,
25 pub time_created: i64,
26 pub time_updated: i64,
27}
28
29#[derive(Debug)]
30pub struct UsageSessionData {
31 pub session: RowUsageSession,
32 pub model: Model,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct MessageCacheTokens {
37 pub write: i64,
38 pub read: i64,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct MessageTokens {
43 pub total: Option<i64>,
45 pub input: i64,
46 pub output: i64,
47 pub reasoning: i64,
48 pub cache: MessageCacheTokens,
49}
50
51#[derive(Debug, Deserialize)]
52pub struct MessageData {
53 pub model: Option<Model>,
55 pub role: Option<String>,
57 #[serde(rename = "providerID")]
58 pub provider_id: Option<String>,
59 #[serde(rename = "modelID")]
60 pub model_id: Option<String>,
61 pub variant: Option<String>,
63 pub cost: Option<f64>,
65 pub tokens: Option<MessageTokens>,
66 pub agent: Option<String>,
67 pub finish: Option<String>,
68}
69
70#[derive(Debug, sqlx::FromRow)]
71pub struct RowSessionMessage {
72 pub id: String,
73 pub session_id: String,
74 pub role: Option<String>,
75 pub time_created: i64,
76 pub time_updated: i64,
77 pub data: String,
78}
79
80#[allow(dead_code)]
81#[derive(Debug, Deserialize)]
82pub struct SessionMessageItem {
83 pub id: String,
84 pub session_id: String,
85 pub cost: f64,
86 pub tokens_input: i64,
87 pub tokens_output: i64,
88 pub tokens_reasoning: i64,
89 pub tokens_cache_read: i64,
90 pub tokens_cache_write: i64,
91 pub time_created: i64,
92 pub time_updated: i64,
93}
94
95#[allow(dead_code)]
96#[derive(Debug)]
97pub struct SessionMessageData {
98 pub message: SessionMessageItem,
99 pub model: Model,
100}
101
102pub struct CollectClient {
103 pool: Option<SqlitePool>,
104}
105
106#[derive(Deserialize, sqlx::FromRow)]
107pub struct TableExists {
108 pub exists: i8,
109}
110
111impl CollectClient {
112 pub fn new() -> Self {
113 CollectClient { pool: None }
114 }
115
116 pub fn get_db_path() -> String {
117 let home = dirs::home_dir().unwrap();
118 let db_path = home
119 .join(".local")
120 .join("share")
121 .join("opencode")
122 .join("opencode.db");
123 db_path.to_str().unwrap().to_string()
124 }
125
126 async fn table_exists(&self, table_name: &str) -> anyhow::Result<bool> {
127 let pool = match &self.pool {
128 Some(pool) => pool,
129 None => {
130 anyhow::bail!("Database pool is not initialized. Call open_pool() first.");
131 }
132 };
133
134 let exists: bool = sqlx::query_scalar(
135 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?)",
136 )
137 .bind(&table_name)
138 .fetch_one(pool)
139 .await?;
140
141 Ok(exists)
142 }
143
144 pub async fn open_pool(&mut self) -> anyhow::Result<()> {
145 let db_path = Self::get_db_path();
146 let pool = SqlitePool::connect(&db_path).await?;
147 self.pool = Some(pool);
148 Ok(())
149 }
150
151 pub async fn get_usage_sessions(
152 &self,
153 min_updated_at: Option<i64>,
154 ) -> anyhow::Result<Vec<UsageSessionData>> {
155 let time_updated = match min_updated_at {
156 Some(value) => value,
157 None => 0, };
159 let pool = match &self.pool {
160 Some(pool) => pool,
161 None => {
162 anyhow::bail!("Database pool is not initialized. Call open_pool() first.");
163 }
164 };
165
166 let use_v2_table = self.table_exists("session_v2").await?;
167
168 let sessions = if use_v2_table {
169 sqlx::query_as::<_, RowUsageSession>(
170 r#"
171 SELECT
172 id,
173 project_id,
174 slug,
175 title,
176 cost,
177 tokens_input,
178 tokens_output,
179 tokens_reasoning,
180 tokens_cache_read,
181 tokens_cache_write,
182 model,
183 time_created,
184 time_updated
185 FROM session_v2
186 WHERE time_updated > ?1
187 ORDER BY time_created DESC
188 "#,
189 )
190 } else {
191 sqlx::query_as::<_, RowUsageSession>(
192 r#"
193 SELECT
194 id,
195 project_id,
196 slug,
197 title,
198 cost,
199 tokens_input,
200 tokens_output,
201 tokens_reasoning,
202 tokens_cache_read,
203 tokens_cache_write,
204 model,
205 time_created,
206 time_updated
207 FROM session
208 WHERE time_updated > ?1
209 ORDER BY time_created DESC
210 "#,
211 )
212 }
213 .bind(time_updated)
214 .fetch_all(pool)
215 .await?;
216
217 let extended_sessions = sessions
218 .into_iter()
219 .map(|session| {
220 let model_result = serde_json::from_str::<Model>(&session.model);
221 let model = match model_result {
222 Ok(model) => model,
223 Err(e) => {
224 eprintln!(
225 "Failed to deserialize model for session {}: {}",
226 session.id, e
227 );
228 Model {
229 id: "unknown".to_string(),
230 provider_id: "unknown".to_string(),
231 variant: None,
232 }
233 }
234 };
235
236 UsageSessionData { session, model }
237 })
238 .collect();
239
240 Ok(extended_sessions)
241 }
242
243 pub async fn get_session_messages(
244 &self,
245 min_updated_at: Option<i64>,
246 ) -> anyhow::Result<Vec<SessionMessageData>> {
247 let time_updated = match min_updated_at {
248 Some(value) => value,
249 None => 0, };
251 let pool = match &self.pool {
252 Some(pool) => pool,
253 None => {
254 anyhow::bail!("Database pool is not initialized. Call open_pool() first.");
255 }
256 };
257
258 let use_v2_table = self.table_exists("session_message").await?;
259 let messages = if use_v2_table {
260 sqlx::query_as::<_, RowSessionMessage>(
261 r#"
262 SELECT
263 id,
264 session_id,
265 type as role,
266 time_created,
267 time_updated,
268 data
269 FROM session_message
270 WHERE time_updated > ?1
271 ORDER BY time_created DESC
272 "#,
273 )
274 } else {
275 sqlx::query_as::<_, RowSessionMessage>(
276 r#"
277 SELECT
278 id,
279 session_id,
280 NULL as role,
281 time_created,
282 time_updated,
283 data
284 FROM message
285 WHERE time_updated > ?1
286 ORDER BY time_created DESC
287 "#,
288 )
289 }
290 .bind(time_updated)
291 .fetch_all(pool)
292 .await?;
293
294 let extended_messages: Vec<SessionMessageData> = messages
295 .into_iter()
296 .filter_map(|message| {
297 let message_result = serde_json::from_str::<MessageData>(&message.data);
298 let message_data = match message_result {
299 Ok(message_data) => message_data,
300 Err(e) => {
301 eprintln!(
302 "Failed to deserialize data for message {}: {}",
303 message.id, e
304 );
305 return None;
306 }
307 };
308
309 let role = match message.role {
310 Some(role) => Some(role),
311 None => message_data.role,
312 };
313 if role.is_none() || role.unwrap() != "assistant" {
314 return None;
315 }
316
317 if message_data.finish.is_some() && message_data.finish.unwrap() == "error" {
318 return None;
319 }
320
321 let model = if let Some(model) = message_data.model {
322 model
323 } else {
324 Model {
325 id: message_data.model_id.unwrap_or("unknown".to_owned()),
326 provider_id: message_data.provider_id.unwrap_or("unknown".to_owned()),
327 variant: message_data.variant,
328 }
329 };
330 if message_data.tokens.is_none() {
331 eprintln!("Tokens are missing for message {}", message.id);
332 return None;
333 }
334
335 let tokens = message_data.tokens.unwrap();
336
337 let message_item = SessionMessageItem {
338 id: message.id,
339 session_id: message.session_id,
340 cost: message_data.cost.unwrap_or(0.0),
341 tokens_input: tokens.input,
342 tokens_output: tokens.output,
343 tokens_reasoning: tokens.reasoning,
344 tokens_cache_read: tokens.cache.read,
345 tokens_cache_write: tokens.cache.write,
346 time_created: message.time_created,
347 time_updated: message.time_updated,
348 };
349
350 Some(SessionMessageData {
351 message: message_item,
352 model,
353 })
354 })
355 .collect();
356
357 Ok(extended_messages)
358 }
359}