use crate::models::PerProviderUsage;
use crate::pricing::{CostSource, ModelPricingMap, resolve_model_cost};
use crate::usage::{StoredCosts, UsageData};
use crate::utils::{extract_token_counts, normalize_usage_value};
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
pub struct PricedUsageRow {
pub cost_usd: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched_model: Option<String>,
pub model: String,
pub usage: Value,
}
pub fn price_usage_data(
usage_data: &UsageData,
pricing_map: &ModelPricingMap,
) -> Vec<PricedUsageRow> {
let mut rows = Vec::with_capacity(usage_data.models.len());
for (model, usage) in usage_data.models.iter() {
let (cost, matched_model) = resolve_merged_model_cost(
model,
&usage_data.per_provider,
pricing_map,
&usage_data.stored_costs,
)
.unwrap_or_else(|| price_usage_value(model, usage, pricing_map, CostSource::Litellm));
rows.push(PricedUsageRow {
model: model.clone(),
usage: normalize_usage_value(usage),
cost_usd: cost,
matched_model,
});
}
rows
}
pub(crate) fn resolve_merged_model_cost(
model: &str,
per_provider: &PerProviderUsage,
pricing_map: &ModelPricingMap,
stored_costs: &StoredCosts,
) -> Option<(f64, Option<String>)> {
let mut total_cost = 0.0;
let mut matched_model = None;
let mut found = false;
for usage in [
&per_provider.claude,
&per_provider.codex,
&per_provider.copilot,
&per_provider.gemini,
] {
if let Some(raw_usage) = usage.get(model) {
found = true;
let (cost, matched) =
price_usage_value(model, raw_usage, pricing_map, CostSource::Litellm);
total_cost += cost;
if matched_model.is_none() {
matched_model = matched;
}
}
}
let stored =
|m: &crate::constants::FastHashMap<String, f64>| m.get(model).copied().unwrap_or(0.0);
for (usage, source) in [
(&per_provider.grok, CostSource::GrokGauge),
(
&per_provider.opencode,
CostSource::OpenCodeStored(stored(&stored_costs.opencode)),
),
(&per_provider.cursor, CostSource::OpenCodeStored(0.0)),
(
&per_provider.hermes,
CostSource::HermesStored(stored(&stored_costs.hermes)),
),
] {
if let Some(raw_usage) = usage.get(model) {
found = true;
let (cost, matched) = price_usage_value(model, raw_usage, pricing_map, source);
total_cost += cost;
if matched_model.is_none() {
matched_model = matched;
}
}
}
found.then_some((total_cost, matched_model))
}
pub(crate) fn price_usage_value(
model: &str,
usage: &Value,
pricing_map: &ModelPricingMap,
source: CostSource,
) -> (f64, Option<String>) {
let counts = extract_token_counts(usage);
resolve_model_cost(model, &counts, pricing_map, source)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{PerProviderUsage, ProviderActiveDays, UsageResult};
use crate::pricing::{ModelPricing, clear_pricing_cache};
use crate::usage::StoredCosts;
use serde_json::json;
use std::collections::HashMap;
#[test]
fn priced_rows_include_grok_source_cost() {
clear_pricing_cache();
let mut raw_pricing = HashMap::new();
raw_pricing.insert(
"shared-model".to_string(),
ModelPricing {
input_cost_per_token: 0.01,
..Default::default()
},
);
let pricing_map = ModelPricingMap::new(raw_pricing);
let mut models = UsageResult::default();
models.insert("shared-model".to_string(), json!({"input_tokens": 200}));
let mut per_provider = PerProviderUsage::default();
per_provider
.claude
.insert("shared-model".to_string(), json!({"input_tokens": 100}));
per_provider
.grok
.insert("shared-model".to_string(), json!({"input_tokens": 100}));
let usage_data = UsageData {
models,
per_provider,
provider_days: ProviderActiveDays::default(),
stored_costs: StoredCosts::default(),
};
let rows = price_usage_data(&usage_data, &pricing_map);
assert!((rows[0].cost_usd - 2.0).abs() < 1e-9);
}
#[test]
fn priced_rows_price_opencode_fallback_only_for_opencode_tokens() {
clear_pricing_cache();
let mut raw_pricing = HashMap::new();
raw_pricing.insert(
"shared".to_string(),
ModelPricing {
input_cost_per_token: 0.01,
..Default::default()
},
);
let pricing_map = ModelPricingMap::new(raw_pricing);
let mut models = UsageResult::default();
models.insert("shared-pro".to_string(), json!({"input_tokens": 200}));
let mut per_provider = PerProviderUsage::default();
per_provider
.claude
.insert("shared-pro".to_string(), json!({"input_tokens": 100}));
per_provider
.opencode
.insert("shared-pro".to_string(), json!({"input_tokens": 100}));
let mut stored_costs = StoredCosts::default();
stored_costs.opencode.insert("shared-pro".to_string(), 7.0);
let usage_data = UsageData {
models,
per_provider,
provider_days: ProviderActiveDays::default(),
stored_costs,
};
let rows = price_usage_data(&usage_data, &pricing_map);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].cost_usd, 8.0);
assert_eq!(rows[0].matched_model.as_deref(), Some("shared"));
}
}