@@ -27,6 +27,9 @@ pub struct Repl {
2727 thinking_budget : u32 ,
2828 session_id : String ,
2929 display_messages : Vec < crate :: history:: DisplayMessage > ,
30+ // Session cost tracking
31+ total_input_tokens : u32 ,
32+ total_output_tokens : u32 ,
3033}
3134
3235impl Repl {
@@ -85,8 +88,101 @@ impl Repl {
8588 thinking_budget,
8689 session_id,
8790 display_messages : Vec :: new ( ) ,
91+ total_input_tokens : 0 ,
92+ total_output_tokens : 0 ,
8893 } )
8994 }
95+
96+ /// Calculate the cost for a given model based on token usage
97+ fn calculate_cost ( model : & str , input_tokens : u32 , output_tokens : u32 ) -> f64 {
98+ // Prices per million tokens in USD
99+ let ( input_price, output_price) = match model {
100+ // Claude 4.5 models
101+ "claude-sonnet-4-5" | "claude-sonnet-4.5" => ( 3.0 , 15.0 ) ,
102+ "claude-haiku-4-5" | "claude-haiku-4.5" => ( 0.8 , 4.0 ) ,
103+
104+ // Claude 4 models
105+ "claude-sonnet-4" | "claude-4-sonnet-20250514" => ( 3.0 , 15.0 ) ,
106+ "claude-opus-4" | "claude-4-opus-20250514" => ( 15.0 , 75.0 ) ,
107+
108+ // Claude 4.1 models
109+ "claude-opus-4.1" | "claude-opus-4-1" => ( 6.0 , 30.0 ) ,
110+
111+ // Claude 3.7 models
112+ "claude-sonnet-3-7" | "claude-sonnet-3.7" => ( 3.0 , 15.0 ) ,
113+
114+ // Claude 3.5 models
115+ "claude-sonnet-3-5" | "claude-sonnet-3.5" => ( 3.0 , 15.0 ) ,
116+ "claude-haiku-3-5" | "claude-haiku-3.5" => ( 0.8 , 4.0 ) ,
117+
118+ // Claude 3 models (legacy)
119+ "claude-opus-3" | "claude-3-opus-20240229" => ( 15.0 , 75.0 ) ,
120+ "claude-sonnet-3" | "claude-3-sonnet-20240229" => ( 3.0 , 15.0 ) ,
121+ "claude-haiku-3" | "claude-3-haiku-20240307" => ( 0.25 , 1.25 ) ,
122+
123+ // Default fallback (use Sonnet 4.5 pricing)
124+ _ => ( 3.0 , 15.0 ) ,
125+ } ;
126+
127+ let input_cost = ( input_tokens as f64 / 1_000_000.0 ) * input_price;
128+ let output_cost = ( output_tokens as f64 / 1_000_000.0 ) * output_price;
129+
130+ input_cost + output_cost
131+ }
132+
133+ /// Format number with thousand separators
134+ fn format_number ( n : u32 ) -> String {
135+ let s = n. to_string ( ) ;
136+ let mut result = String :: new ( ) ;
137+ for ( i, c) in s. chars ( ) . rev ( ) . enumerate ( ) {
138+ if i > 0 && i % 3 == 0 {
139+ result. push ( ',' ) ;
140+ }
141+ result. push ( c) ;
142+ }
143+ result. chars ( ) . rev ( ) . collect ( )
144+ }
145+
146+ /// Display session summary with cost information
147+ fn display_session_summary ( & self ) {
148+ if self . total_input_tokens == 0 && self . total_output_tokens == 0 {
149+ // No API calls made in this session
150+ return ;
151+ }
152+
153+ println ! ( ) ;
154+ println ! ( "{}" , "─" . repeat( 50 ) . bright_cyan( ) ) ;
155+ println ! ( "{}" , "Session Summary" . bright_cyan( ) . bold( ) ) ;
156+ println ! ( "{}" , "─" . repeat( 50 ) . bright_cyan( ) ) ;
157+
158+ // Calculate estimated cost based on model pricing
159+ let estimated_cost = Self :: calculate_cost (
160+ & self . model ,
161+ self . total_input_tokens ,
162+ self . total_output_tokens ,
163+ ) ;
164+
165+ println ! ( "{:<20} {}" ,
166+ "Input tokens:" . bright_white( ) ,
167+ Self :: format_number( self . total_input_tokens) . bright_green( )
168+ ) ;
169+ println ! ( "{:<20} {}" ,
170+ "Output tokens:" . bright_white( ) ,
171+ Self :: format_number( self . total_output_tokens) . bright_green( )
172+ ) ;
173+ println ! ( "{:<20} {}" ,
174+ "Total tokens:" . bright_white( ) ,
175+ Self :: format_number( self . total_input_tokens + self . total_output_tokens) . bright_green( )
176+ ) ;
177+ println ! ( ) ;
178+ println ! ( "{:<20} {}" ,
179+ "Estimated cost:" . bright_white( ) . bold( ) ,
180+ format!( "${:.4}" , estimated_cost) . bright_yellow( ) . bold( )
181+ ) ;
182+
183+ println ! ( "{}" , "─" . repeat( 50 ) . bright_cyan( ) ) ;
184+ println ! ( ) ;
185+ }
90186
91187 pub fn run ( & mut self ) -> Result < ( ) > {
92188 println ! ( "{}" , "Sofos - AI Coding Assistant" . bright_cyan( ) . bold( ) ) ;
@@ -112,7 +208,9 @@ impl Repl {
112208 match line. to_lowercase ( ) . as_str ( ) {
113209 "exit" | "quit" => {
114210 self . save_current_session ( ) ?;
115- println ! ( "\n {}" , "Goodbye!" . bright_cyan( ) ) ;
211+ self . display_session_summary ( ) ;
212+
213+ println ! ( "{}" , "Goodbye!" . bright_cyan( ) ) ;
116214 break ;
117215 }
118216 "clear" => {
@@ -159,10 +257,8 @@ impl Repl {
159257
160258 if let Err ( e) = self . process_message ( line) {
161259 eprintln ! ( "{} {}" , "Error:" . bright_red( ) . bold( ) , e) ;
162- } else {
163- if let Err ( e) = self . save_current_session ( ) {
164- eprintln ! ( "{} Failed to save session: {}" , "Warning:" . bright_yellow( ) , e) ;
165- }
260+ } else if let Err ( e) = self . save_current_session ( ) {
261+ eprintln ! ( "{} Failed to save session: {}" , "Warning:" . bright_yellow( ) , e) ;
166262 }
167263
168264 println ! ( ) ;
@@ -172,6 +268,8 @@ impl Repl {
172268 }
173269 Err ( ReadlineError :: Eof ) => {
174270 self . save_current_session ( ) ?;
271+ self . display_session_summary ( ) ;
272+
175273 println ! ( "{}" , "Goodbye!" . bright_cyan( ) ) ;
176274 break ;
177275 }
@@ -267,6 +365,10 @@ impl Repl {
267365 }
268366
269367 let response = response?;
368+
369+ // Track token usage
370+ self . total_input_tokens += response. usage . input_tokens ;
371+ self . total_output_tokens += response. usage . output_tokens ;
270372
271373 self . handle_response ( response. content , & runtime) ?;
272374
@@ -311,7 +413,7 @@ impl Repl {
311413
312414 // Track this as a system message in display
313415 self . display_messages . push ( crate :: history:: DisplayMessage :: UserMessage {
314- content : format ! ( "[System: Maximum tool iterations reached]" ) ,
416+ content : "[System: Maximum tool iterations reached]" . to_string ( ) ,
315417 } ) ;
316418
317419 // Let Claude respond to the interruption
@@ -660,6 +762,11 @@ impl Repl {
660762 e
661763 ) ;
662764 }
765+
766+ // Track token usage
767+ self . total_input_tokens += resp. usage . input_tokens ;
768+ self . total_output_tokens += resp. usage . output_tokens ;
769+
663770 resp
664771 }
665772 Err ( e) => {
@@ -753,6 +860,8 @@ impl Repl {
753860 println ! ( ) ;
754861 self . process_message ( prompt) ?;
755862 self . save_current_session ( ) ?;
863+ self . display_session_summary ( ) ;
864+
756865 Ok ( ( ) )
757866 }
758867
@@ -843,7 +952,7 @@ impl Repl {
843952 crate :: history:: DisplayMessage :: ToolExecution { tool_name, tool_input : _, tool_output } => {
844953 if tool_name == "execute_bash" {
845954 if let Ok ( input_val) = serde_json:: from_value :: < serde_json:: Value > (
846- serde_json:: to_value ( & tool_output) . unwrap_or_default ( )
955+ serde_json:: to_value ( tool_output) . unwrap_or_default ( )
847956 ) {
848957 if let Some ( command) = input_val. get ( "command" ) . and_then ( |v| v. as_str ( ) ) {
849958 println ! (
@@ -891,7 +1000,7 @@ impl Repl {
8911000
8921001 if line_count == 0 {
8931002 if file_path. is_empty ( ) {
894- format ! ( "Read file (empty or not found)" )
1003+ "Read file (empty or not found)" . to_string ( )
8951004 } else {
8961005 format ! ( "Read file from {} - empty or not found" , file_path. bright_cyan( ) )
8971006 }
0 commit comments