@@ -202,23 +202,23 @@ fn get_cortex_home() -> PathBuf {
202202fn get_model_pricing ( model : & str ) -> ModelPricing {
203203 // First check for custom pricing from environment
204204 let custom_pricing = load_custom_pricing ( ) ;
205- let model_lower = model . to_lowercase ( ) ;
205+ let model_key = normalize_model_key ( model ) ;
206206
207207 // Check for exact match in custom pricing
208- if let Some ( pricing) = custom_pricing. get ( & model_lower ) {
208+ if let Some ( pricing) = custom_pricing. get ( & model_key ) {
209209 return pricing. clone ( ) ;
210210 }
211211
212212 // Check for partial match in custom pricing (e.g., "gpt-4o" matches "gpt-4o-mini")
213213 for ( key, pricing) in & custom_pricing {
214- if model_lower . contains ( key) {
214+ if model_key . contains ( key) {
215215 return pricing. clone ( ) ;
216216 }
217217 }
218218
219219 // Fall back to default pricing (may be outdated - users can override via CORTEX_PRICING_*)
220220 // Pricing per 1M tokens (as of late 2024/early 2025 - may change)
221- match model {
221+ match model_key . as_str ( ) {
222222 // Anthropic
223223 m if m. contains ( "claude-opus-4" ) || m. contains ( "opus-4" ) => ModelPricing {
224224 input_per_million : 15.0 ,
@@ -294,6 +294,10 @@ fn get_model_pricing(model: &str) -> ModelPricing {
294294 }
295295}
296296
297+ fn normalize_model_key ( model : & str ) -> String {
298+ model. to_lowercase ( )
299+ }
300+
297301/// Calculate cost for token usage.
298302fn calculate_cost ( model : & str , input_tokens : u64 , output_tokens : u64 ) -> f64 {
299303 let pricing = get_model_pricing ( model) ;
@@ -406,14 +410,13 @@ async fn collect_stats(sessions_dir: &PathBuf, cli: &StatsCli) -> Result<UsageSt
406410 provider_stats. estimated_cost_usd += session_cost;
407411
408412 // Per-model stats
409- let model_stats =
410- stats
411- . by_model
412- . entry ( model. to_string ( ) )
413- . or_insert_with ( || ModelStats {
414- provider : provider. clone ( ) ,
415- ..Default :: default ( )
416- } ) ;
413+ let model_stats = stats
414+ . by_model
415+ . entry ( normalize_model_key ( model) )
416+ . or_insert_with ( || ModelStats {
417+ provider : provider. clone ( ) ,
418+ ..Default :: default ( )
419+ } ) ;
417420 model_stats. sessions += 1 ;
418421 model_stats. messages += session_data. message_count ;
419422 model_stats. input_tokens += session_data. input_tokens ;
@@ -733,6 +736,56 @@ mod tests {
733736 // GPT-4o: $2.50/$10 per 1M
734737 let cost = calculate_cost ( "gpt-4o" , 1_000_000 , 1_000_000 ) ;
735738 assert ! ( ( cost - 12.5 ) . abs( ) < 0.001 ) ;
739+
740+ // Model pricing should be case-insensitive.
741+ let cost = calculate_cost ( "GPT-4O" , 1_000_000 , 1_000_000 ) ;
742+ assert ! ( ( cost - 12.5 ) . abs( ) < 0.001 ) ;
743+ }
744+
745+ #[ tokio:: test]
746+ async fn test_collect_stats_normalizes_model_case ( ) {
747+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
748+ let sessions_dir = temp_dir. path ( ) . to_path_buf ( ) ;
749+
750+ std:: fs:: write (
751+ sessions_dir. join ( "lower.json" ) ,
752+ r#"{
753+ "created_at": "2026-04-09T00:00:00Z",
754+ "model": "gpt-4o",
755+ "messages": [{"role": "user", "content": "a"}],
756+ "usage": {"input_tokens": 100, "output_tokens": 100}
757+ }"# ,
758+ )
759+ . unwrap ( ) ;
760+
761+ std:: fs:: write (
762+ sessions_dir. join ( "upper.json" ) ,
763+ r#"{
764+ "created_at": "2026-04-09T00:01:00Z",
765+ "model": "GPT-4O",
766+ "messages": [{"role": "user", "content": "b"}],
767+ "usage": {"input_tokens": 200, "output_tokens": 200}
768+ }"# ,
769+ )
770+ . unwrap ( ) ;
771+
772+ let cli = StatsCli {
773+ days : 3650 ,
774+ provider : None ,
775+ model : None ,
776+ json : false ,
777+ verbose : true ,
778+ } ;
779+
780+ let stats = collect_stats ( & sessions_dir, & cli) . await . unwrap ( ) ;
781+
782+ assert_eq ! ( stats. total_sessions, 2 ) ;
783+ assert_eq ! ( stats. by_model. len( ) , 1 ) ;
784+ let model_stats = stats. by_model . get ( "gpt-4o" ) . unwrap ( ) ;
785+ assert_eq ! ( model_stats. sessions, 2 ) ;
786+ assert_eq ! ( model_stats. messages, 2 ) ;
787+ assert_eq ! ( model_stats. input_tokens, 300 ) ;
788+ assert_eq ! ( model_stats. output_tokens, 300 ) ;
736789 }
737790
738791 #[ test]
0 commit comments