use super::{Cost, Usage};
use serde_json::Value;
pub fn combine_usage(a: &Usage, b: &Usage) -> Usage {
Usage {
input: a.input.saturating_add(b.input),
output: a.output.saturating_add(b.output),
cache_read: a.cache_read.saturating_add(b.cache_read),
cache_write: a.cache_write.saturating_add(b.cache_write),
cache_write_1h: match (a.cache_write_1h, b.cache_write_1h) {
(None, None) => None,
(x, y) => Some(x.unwrap_or(0).saturating_add(y.unwrap_or(0))),
},
reasoning: match (a.reasoning, b.reasoning) {
(None, None) => None,
(x, y) => Some(x.unwrap_or(0).saturating_add(y.unwrap_or(0))),
},
total_tokens: a.total_tokens.saturating_add(b.total_tokens),
cost: Cost {
input: a.cost.input + b.cost.input,
output: a.cost.output + b.cost.output,
cache_read: a.cost.cache_read + b.cost.cache_read,
cache_write: a.cost.cache_write + b.cost.cache_write,
total: a.cost.total + b.cost.total,
},
}
}
pub fn compute_cost(usage: &mut Usage, cost_cfg: Option<&Value>) {
let Some(cost) = cost_cfg else {
usage.cost = Cost::default();
return;
};
let input_tokens = usage.input + usage.cache_read + usage.cache_write;
let mut rates = cost;
let mut matched_threshold: f64 = -1.0;
if let Some(tiers) = cost.get("tiers").and_then(|v| v.as_array()) {
for tier in tiers {
let above = tier
.get("inputTokensAbove")
.and_then(|v| v.as_f64())
.unwrap_or(-1.0);
if input_tokens as f64 > above && above > matched_threshold {
rates = tier;
matched_threshold = above;
}
}
}
let i_price = cost_rate(rates, "input");
let o_price = cost_rate(rates, "output");
let cr_price = cost_rate(rates, "cacheRead");
let cw_price = cost_rate(rates, "cacheWrite");
let long_write = usage.cache_write_1h.unwrap_or(0);
let short_write = usage.cache_write.saturating_sub(long_write);
let i = usage.input as f64 / 1e6 * i_price;
let o = usage.output as f64 / 1e6 * o_price;
let cr = usage.cache_read as f64 / 1e6 * cr_price;
let cw = (cw_price * short_write as f64 + i_price * 2.0 * long_write as f64) / 1e6;
usage.cost = Cost {
input: i,
output: o,
cache_read: cr,
cache_write: cw,
total: i + o + cr + cw,
};
}
pub fn service_tier_cost_multiplier(model_id: &str, service_tier: Option<&str>) -> f64 {
match service_tier {
Some("flex") => 0.5,
Some("priority") | Some("fast") => {
if model_id == "gpt-5.5" {
2.5
} else {
2.0
}
}
_ => 1.0,
}
}
pub fn apply_service_tier_cost(usage: &mut Usage, model_id: &str, service_tier: Option<&str>) {
let multiplier = service_tier_cost_multiplier(model_id, service_tier);
if multiplier == 1.0 {
return;
}
usage.cost.input *= multiplier;
usage.cost.output *= multiplier;
usage.cost.cache_read *= multiplier;
usage.cost.cache_write *= multiplier;
usage.cost.total =
usage.cost.input + usage.cost.output + usage.cost.cache_read + usage.cost.cache_write;
}
fn cost_rate(cost: &Value, key: &str) -> f64 {
cost.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn cost_uses_usage_split_and_tiers() {
let mut usage = Usage {
input: 500,
output: 200,
cache_read: 100,
cache_write: 50,
cache_write_1h: Some(20),
total_tokens: 850,
cost: Cost::default(),
reasoning: None,
};
let cost = json!({
"input": 1.0,
"output": 2.0,
"cacheRead": 0.5,
"cacheWrite": 0.25,
"tiers": [{
"inputTokensAbove": 400,
"input": 0.5,
"output": 1.0,
"cacheRead": 0.2,
"cacheWrite": 0.1
}]
});
compute_cost(&mut usage, Some(&cost));
assert!((usage.cost.input - 0.00025).abs() < 1e-12);
assert!((usage.cost.output - 0.0002).abs() < 1e-12);
assert!((usage.cost.cache_write - (0.1 * 30.0 + 0.5 * 2.0 * 20.0) / 1e6).abs() < 1e-12);
compute_cost(&mut usage, None);
assert_eq!(usage.cost.total, 0.0);
}
#[test]
fn service_tier_multiplier_matches_pi() {
assert_eq!(
service_tier_cost_multiplier("gpt-6-luna", Some("fast")),
2.0
);
assert_eq!(
service_tier_cost_multiplier("gpt-6-luna", Some("priority")),
2.0
);
assert_eq!(service_tier_cost_multiplier("gpt-5.5", Some("fast")), 2.5);
assert_eq!(service_tier_cost_multiplier("gpt-5.5", Some("flex")), 0.5);
assert_eq!(
service_tier_cost_multiplier("gpt-6-luna", Some("default")),
1.0
);
assert_eq!(service_tier_cost_multiplier("gpt-6-luna", None), 1.0);
let mut usage = Usage {
input: 1_000_000,
output: 1_000_000,
cache_read: 1_000_000,
cache_write: 1_000_000,
cache_write_1h: None,
total_tokens: 4_000_000,
cost: Cost::default(),
reasoning: None,
};
let cost = json!({ "input": 2.0, "output": 10.0, "cacheRead": 0.1, "cacheWrite": 2.5 });
compute_cost(&mut usage, Some(&cost));
apply_service_tier_cost(&mut usage, "gpt-6-luna", Some("fast"));
assert!((usage.cost.input - 4.0).abs() < 1e-12);
assert!((usage.cost.output - 20.0).abs() < 1e-12);
assert!((usage.cost.cache_read - 0.2).abs() < 1e-12);
assert!((usage.cost.cache_write - 5.0).abs() < 1e-12);
assert!((usage.cost.total - 29.2).abs() < 1e-12);
let mut std_usage = usage.clone();
std_usage.cost = Cost::default();
compute_cost(&mut std_usage, Some(&cost));
let before = std_usage.cost.total;
apply_service_tier_cost(&mut std_usage, "gpt-6-luna", Some("auto"));
assert!((std_usage.cost.total - before).abs() < 1e-12);
}
}