@@ -12,10 +12,11 @@ use std::{
1212use jiff:: tz:: TimeZone as JiffTimeZone ;
1313use memchr:: memmem;
1414use rustc_hash:: FxHasher ;
15+ use serde:: Deserialize ;
1516
1617use crate :: {
17- LoadedEntry , LoadedFile , PricingMap , Result , Speed , TimestampMs , UsageEntry , UsageSummary ,
18- calculate_cost,
18+ LoadedEntry , LoadedFile , PricingMap , Result , Speed , TimestampMs , TokenUsageRaw , UsageEntry ,
19+ UsageSummary , calculate_cost, calculate_cost_for_usage ,
1920 cli:: { CostMode , SharedArgs } ,
2021 debug_log,
2122 fast:: { FxHashMap , SmallIndexVec , byte_lines, suffix_string} ,
@@ -385,7 +386,7 @@ fn read_usage_file(
385386 Some ( model. clone ( ) )
386387 }
387388 } ) ;
388- loaded_file . entries . push ( LoadedEntry {
389+ let entry = LoadedEntry {
389390 data,
390391 timestamp,
391392 date,
@@ -399,11 +400,107 @@ fn read_usage_file(
399400 model,
400401 usage_limit_reset_time,
401402 missing_pricing_model,
402- } ) ;
403+ } ;
404+ let mut advisor_entries = Vec :: new ( ) ;
405+ for ( index, advisor) in advisor_usages_from_line ( line) . into_iter ( ) . enumerate ( ) {
406+ let mut advisor_data = entry. data . clone ( ) ;
407+ advisor_data. message . id = advisor_data
408+ . message
409+ . id
410+ . map ( |message_id| format ! ( "{message_id}:advisor:{index}" ) ) ;
411+ advisor_data. message . model = Some ( advisor. model . clone ( ) ) ;
412+ advisor_data. message . usage = advisor. usage ;
413+ advisor_data. cost_usd = None ;
414+ let missing_pricing_model = missing_pricing_model_for_usage (
415+ Some ( & advisor. model ) ,
416+ advisor. usage ,
417+ None ,
418+ mode,
419+ pricing,
420+ ) ;
421+ advisor_entries. push ( LoadedEntry {
422+ data : advisor_data,
423+ timestamp,
424+ date : entry. date . clone ( ) ,
425+ project : Arc :: clone ( & project) ,
426+ session_id : Arc :: clone ( & session_id) ,
427+ project_path : Arc :: clone ( & project_path) ,
428+ cost : calculate_cost_for_usage (
429+ Some ( & advisor. model ) ,
430+ advisor. usage ,
431+ None ,
432+ mode,
433+ pricing,
434+ ) ,
435+ extra_total_tokens : 0 ,
436+ credits : None ,
437+ message_count : None ,
438+ model : Some ( advisor. model ) ,
439+ usage_limit_reset_time,
440+ missing_pricing_model,
441+ } ) ;
442+ }
443+ loaded_file. entries . push ( entry) ;
444+ loaded_file. entries . extend ( advisor_entries) ;
403445 }
404446 loaded_file
405447}
406448
449+ #[ derive( Debug , Deserialize ) ]
450+ struct UsageIterationsEnvelope {
451+ message : UsageIterationsMessage ,
452+ }
453+
454+ #[ derive( Debug , Deserialize ) ]
455+ struct UsageIterationsMessage {
456+ usage : UsageIterations ,
457+ }
458+
459+ #[ derive( Debug , Deserialize ) ]
460+ struct UsageIterations {
461+ #[ serde( default ) ]
462+ iterations : Vec < UsageIteration > ,
463+ }
464+
465+ #[ derive( Debug , Deserialize ) ]
466+ struct UsageIteration {
467+ #[ serde( rename = "type" ) ]
468+ kind : String ,
469+ model : Option < String > ,
470+ #[ serde( flatten) ]
471+ usage : TokenUsageRaw ,
472+ }
473+
474+ pub ( super ) struct AdvisorUsage {
475+ pub ( super ) model : String ,
476+ pub ( super ) usage : TokenUsageRaw ,
477+ }
478+
479+ pub ( super ) fn advisor_usages_from_line ( line : & [ u8 ] ) -> Vec < AdvisorUsage > {
480+ if memmem:: find ( line, br#""advisor_message""# ) . is_none ( ) {
481+ return Vec :: new ( ) ;
482+ }
483+ let Ok ( envelope) = serde_json:: from_slice :: < UsageIterationsEnvelope > ( line) else {
484+ return Vec :: new ( ) ;
485+ } ;
486+ envelope
487+ . message
488+ . usage
489+ . iterations
490+ . into_iter ( )
491+ . filter_map ( |iteration| {
492+ ( iteration. kind == "advisor_message" )
493+ . then_some ( iteration. model )
494+ . flatten ( )
495+ . filter ( |model| !model. is_empty ( ) )
496+ . map ( |model| AdvisorUsage {
497+ model,
498+ usage : iteration. usage ,
499+ } )
500+ } )
501+ . collect ( )
502+ }
503+
407504fn update_loaded_file_timestamp ( loaded_file : & mut LoadedFile , timestamp : TimestampMs ) {
408505 loaded_file. timestamp = Some (
409506 loaded_file
@@ -563,9 +660,12 @@ mod tests {
563660
564661 use super :: {
565662 extract_session_parts, has_unsupported_null_field, paths:: is_project_path_segment,
566- push_deduped_entry, usage_files,
663+ push_deduped_entry, read_usage_file, usage_files,
664+ } ;
665+ use crate :: {
666+ LoadedEntry , PricingMap , TimestampMs , TokenUsageRaw , UsageEntry , UsageMessage ,
667+ cli:: CostMode ,
567668 } ;
568- use crate :: { LoadedEntry , TimestampMs , TokenUsageRaw , UsageEntry , UsageMessage } ;
569669 use ccusage_test_support:: fs_fixture;
570670
571671 #[ test]
@@ -653,6 +753,38 @@ mod tests {
653753 ) ) ;
654754 }
655755
756+ #[ test]
757+ fn calculates_advisor_cost_with_the_advisor_model ( ) {
758+ let fixture = fs_fixture ! ( {
759+ "projects/project-a/session-a/chat.jsonl" : r#"{"timestamp":"2026-05-22T02:34:40.000Z","version":"1.2.3","sessionId":"session-a","message":{"id":"msg-parent","model":"main-model","usage":{"input_tokens":1,"output_tokens":2,"iterations":[{"type":"advisor_message","model":"advisor-model","input_tokens":10,"output_tokens":2,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}]}},"requestId":"req-parent","costUSD":1.23}"# ,
760+ } ) ;
761+ let mut pricing = PricingMap :: default ( ) ;
762+ pricing. load_json (
763+ r#"{
764+ "main-model": {
765+ "input_cost_per_token": 100,
766+ "output_cost_per_token": 100
767+ },
768+ "advisor-model": {
769+ "input_cost_per_token": 2,
770+ "output_cost_per_token": 3
771+ }
772+ }"# ,
773+ ) ;
774+
775+ let loaded = read_usage_file (
776+ & fixture. path ( "projects/project-a/session-a/chat.jsonl" ) ,
777+ None ,
778+ CostMode :: Auto ,
779+ Some ( & pricing) ,
780+ ) ;
781+
782+ assert_eq ! ( loaded. entries. len( ) , 2 ) ;
783+ assert_eq ! ( loaded. entries[ 0 ] . cost, 1.23 ) ;
784+ assert_eq ! ( loaded. entries[ 1 ] . model. as_deref( ) , Some ( "advisor-model" ) ) ;
785+ assert_eq ! ( loaded. entries[ 1 ] . cost, 26.0 ) ;
786+ }
787+
656788 #[ test]
657789 fn keeps_parent_usage_when_sidechain_replays_message_with_new_request_id ( ) {
658790 let mut deduped_indexes = Default :: default ( ) ;
0 commit comments