Skip to content

Commit 1e9ac5a

Browse files
committed
fix(cli): normalize stats model aggregation
1 parent 7954d02 commit 1e9ac5a

1 file changed

Lines changed: 65 additions & 12 deletions

File tree

src/cortex-cli/src/stats_cmd.rs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,23 +202,23 @@ fn get_cortex_home() -> PathBuf {
202202
fn 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.
298302
fn 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

Comments
 (0)