use layover_core::cost::CostSource;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq)]
pub struct Reported {
pub usd: f64,
pub source: CostSource,
pub input_tokens: u64,
pub output_tokens: u64,
}
impl Reported {
#[must_use]
pub const fn unreported() -> Self {
Self {
usd: 0.0,
source: CostSource::Unreported,
input_tokens: 0,
output_tokens: 0,
}
}
}
#[derive(Debug, Deserialize)]
struct Line {
#[serde(alias = "total_cost_usd", alias = "cost_usd", alias = "costUSD")]
cost: Option<f64>,
usage: Option<Usage>,
}
#[derive(Debug, Deserialize)]
struct Usage {
#[serde(alias = "input_tokens", alias = "prompt_tokens")]
input: Option<u64>,
#[serde(alias = "output_tokens", alias = "completion_tokens")]
output: Option<u64>,
}
const TOKENS_PER_DOLLAR: f64 = 1_000_000.0;
#[must_use]
pub fn from_transcript(text: &str) -> Reported {
let mut best: Option<Reported> = None;
for line in text.lines() {
let line = line.trim();
if !line.starts_with('{') {
continue;
}
let Ok(parsed) = serde_json::from_str::<Line>(line) else {
continue;
};
let input = parsed.usage.as_ref().and_then(|u| u.input).unwrap_or(0);
let output = parsed.usage.as_ref().and_then(|u| u.output).unwrap_or(0);
let Some(usd) = parsed.cost else {
if input + output > 0 {
best = Some(Reported {
usd: 0.0,
source: CostSource::Unreported,
input_tokens: input,
output_tokens: output,
});
}
continue;
};
best = Some(judge(usd, input, output));
}
best.unwrap_or_else(Reported::unreported)
}
fn judge(usd: f64, input: u64, output: u64) -> Reported {
let tokens = input + output;
if !usd.is_finite() || usd < 0.0 {
return Reported {
usd: 0.0,
source: CostSource::Unreported,
input_tokens: input,
output_tokens: output,
};
}
if usd == 0.0 && tokens > 0 {
return Reported {
usd: 0.0,
source: CostSource::Unreported,
input_tokens: input,
output_tokens: output,
};
}
if tokens > 0 {
#[expect(
clippy::cast_precision_loss,
reason = "token counts never approach 2^53"
)]
let implied = tokens as f64 / TOKENS_PER_DOLLAR;
if usd > 0.0 && usd * 10.0 < implied {
return Reported {
usd: 0.0,
source: CostSource::Unreported,
input_tokens: input,
output_tokens: output,
};
}
}
Reported {
usd,
source: CostSource::Reported,
input_tokens: input,
output_tokens: output,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_plain_total_is_read() {
let got = from_transcript(r#"{"total_cost_usd": 1.25, "usage": {"input_tokens": 900000}}"#);
assert!((got.usd - 1.25).abs() < f64::EPSILON);
assert_eq!(got.source, CostSource::Reported);
assert_eq!(got.input_tokens, 900_000);
}
#[test]
fn the_last_total_wins_because_runners_emit_running_ones() {
let text = "\
{\"total_cost_usd\": 0.10, \"usage\": {\"input_tokens\": 100000}}
the agent says something
{\"total_cost_usd\": 0.90, \"usage\": {\"input_tokens\": 800000}}";
assert!((from_transcript(text).usd - 0.90).abs() < f64::EPSILON);
}
#[test]
fn prose_between_the_json_is_ignored() {
let text = "Thinking about it.\nI will run the suite.\n{\"cost_usd\": 0.4, \"usage\": {\"input_tokens\": 350000}}\nDone.";
assert!((from_transcript(text).usd - 0.4).abs() < f64::EPSILON);
}
#[test]
fn a_transcript_with_no_numbers_is_unreported_not_free() {
let got = from_transcript("I looked at the file and it seemed fine.");
assert_eq!(got.source, CostSource::Unreported);
assert!(got.usd.abs() < f64::EPSILON);
}
#[test]
fn zero_dollars_with_real_tokens_is_silence() {
let got = from_transcript(r#"{"total_cost_usd": 0.0, "usage": {"input_tokens": 50000}}"#);
assert_eq!(got.source, CostSource::Unreported);
assert_eq!(
got.input_tokens, 50_000,
"the tokens are still worth keeping"
);
}
#[test]
fn a_negative_cost_cannot_credit_fuel_back() {
let got = from_transcript(r#"{"total_cost_usd": -5.0, "usage": {"output_tokens": 1000}}"#);
assert!(got.usd.abs() < f64::EPSILON);
assert_eq!(got.source, CostSource::Unreported);
}
#[test]
fn a_cost_wildly_below_what_the_tokens_imply_is_not_believed() {
let got =
from_transcript(r#"{"total_cost_usd": 0.01, "usage": {"input_tokens": 4000000}}"#);
assert_eq!(
got.source,
CostSource::Unreported,
"four million tokens does not cost a cent"
);
}
#[test]
fn a_cost_merely_cheaper_than_the_yardstick_is_still_believed() {
let got = from_transcript(r#"{"total_cost_usd": 0.5, "usage": {"input_tokens": 1000000}}"#);
assert_eq!(got.source, CostSource::Reported);
assert!((got.usd - 0.5).abs() < f64::EPSILON);
}
#[test]
fn tokens_without_a_cost_still_record_that_work_happened() {
let got = from_transcript(r#"{"usage": {"input_tokens": 1200, "output_tokens": 300}}"#);
assert_eq!(got.source, CostSource::Unreported);
assert_eq!(got.input_tokens, 1200);
assert_eq!(got.output_tokens, 300);
}
#[test]
fn malformed_json_does_not_stop_the_readable_lines_being_read() {
let text = "{\"total_cost_usd\": oops}\n{\"total_cost_usd\": 2.0, \"usage\": {\"input_tokens\": 1900000}}";
assert!((from_transcript(text).usd - 2.0).abs() < f64::EPSILON);
}
#[test]
fn an_alternative_field_name_is_accepted() {
for text in [
r#"{"total_cost_usd": 3.0, "usage": {"input_tokens": 2900000}}"#,
r#"{"cost_usd": 3.0, "usage": {"prompt_tokens": 2900000}}"#,
r#"{"costUSD": 3.0, "usage": {"input": 2900000}}"#,
] {
assert!(
(from_transcript(text).usd - 3.0).abs() < f64::EPSILON,
"{text}"
);
}
}
}