@@ -34,6 +34,15 @@ pub struct CodexSessionRecord {
3434 pub updated_at : Option < i64 > ,
3535 pub location_count : usize ,
3636 pub locations : Vec < CodexSessionLocation > ,
37+ /// 输入token数量(来自rollout文件的token_count记录)
38+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
39+ pub input_tokens : Option < u64 > ,
40+ /// 输出token数量(来自rollout文件的token_count记录)
41+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
42+ pub output_tokens : Option < u64 > ,
43+ /// 总token数量(来自rollout文件的token_count记录)
44+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
45+ pub total_tokens : Option < u64 > ,
3746}
3847
3948#[ derive( Debug , Clone , Serialize ) ]
@@ -148,6 +157,54 @@ struct TrashedSessionEntry {
148157 trashed_rollout_path : PathBuf ,
149158}
150159
160+ /// 从 rollout JSONL 文件中读取 token 统计信息
161+ /// 返回 (input_tokens, output_tokens, total_tokens)
162+ fn read_token_stats_from_rollout ( rollout_path : & Path ) -> Option < ( u64 , u64 , u64 ) > {
163+ let content = fs:: read_to_string ( rollout_path) . ok ( ) ?;
164+
165+ // 从文件末尾向前读取,找到第一条 token_count 记录
166+ // token_count 记录通常在文件末尾
167+ for line in content. lines ( ) . rev ( ) {
168+ let trimmed = line. trim ( ) ;
169+ if trimmed. is_empty ( ) {
170+ continue ;
171+ }
172+
173+ if let Ok ( parsed) = serde_json:: from_str :: < JsonValue > ( trimmed) {
174+ // 检查 type == "event_msg"
175+ if parsed. get ( "type" ) . and_then ( |v| v. as_str ( ) ) != Some ( "event_msg" ) {
176+ continue ;
177+ }
178+
179+ if let Some ( payload) = parsed. get ( "payload" ) {
180+ // 检查 payload.type == "token_count"
181+ if payload. get ( "type" ) . and_then ( |v| v. as_str ( ) ) != Some ( "token_count" ) {
182+ continue ;
183+ }
184+
185+ if let Some ( info) = payload. get ( "info" ) {
186+ if let Some ( usage) = info. get ( "total_token_usage" ) {
187+ let input = usage
188+ . get ( "input_tokens" )
189+ . and_then ( |v| v. as_u64 ( ) )
190+ . unwrap_or ( 0 ) ;
191+ let output = usage
192+ . get ( "output_tokens" )
193+ . and_then ( |v| v. as_u64 ( ) )
194+ . unwrap_or ( 0 ) ;
195+ let total = usage
196+ . get ( "total_tokens" )
197+ . and_then ( |v| v. as_u64 ( ) )
198+ . unwrap_or ( 0 ) ;
199+ return Some ( ( input, output, total) ) ;
200+ }
201+ }
202+ }
203+ }
204+ }
205+ None
206+ }
207+
151208pub fn list_sessions_across_instances ( ) -> Result < Vec < CodexSessionRecord > , String > {
152209 let instances = collect_instances ( ) ?;
153210 let process_entries = modules:: process:: collect_codex_process_entries ( ) ;
@@ -156,6 +213,9 @@ pub fn list_sessions_across_instances() -> Result<Vec<CodexSessionRecord>, Strin
156213 for instance in & instances {
157214 let running = is_instance_running ( instance, & process_entries) ;
158215 for snapshot in load_thread_snapshots ( instance) ? {
216+ // 读取 token 统计
217+ let token_stats = read_token_stats_from_rollout ( & snapshot. rollout_path ) ;
218+
159219 let entry =
160220 session_map
161221 . entry ( snapshot. id . clone ( ) )
@@ -166,6 +226,9 @@ pub fn list_sessions_across_instances() -> Result<Vec<CodexSessionRecord>, Strin
166226 updated_at : snapshot. updated_at ,
167227 location_count : 0 ,
168228 locations : Vec :: new ( ) ,
229+ input_tokens : token_stats. as_ref ( ) . map ( |( i, _, _) | * i) ,
230+ output_tokens : token_stats. as_ref ( ) . map ( |( _, o, _) | * o) ,
231+ total_tokens : token_stats. as_ref ( ) . map ( |( _, _, t) | * t) ,
169232 } ) ;
170233
171234 if entry. updated_at . is_none ( ) {
0 commit comments