use super::model::{DEFAULT_FALLBACK_MODEL, UsageTotals};
use super::scan_runtime::ScanObserver;
use chrono::{DateTime, Utc};
use eyre::{Result, WrapErr};
use serde::Deserialize;
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::marker::PhantomData;
use std::path::Path;
#[derive(Clone, Copy, Debug, Default)]
pub(in crate::app) struct RawUsage {
pub(in crate::app) input: u64,
pub(in crate::app) cached_input: u64,
pub(in crate::app) output: u64,
pub(in crate::app) reasoning_output: u64,
pub(in crate::app) total: u64,
}
impl RawUsage {
fn into_usage_totals(self) -> UsageTotals {
UsageTotals {
input: self.input,
cached_input: self.cached_input.min(self.input),
output: self.output,
reasoning_output: self.reasoning_output,
total: if self.total > 0 {
self.total
} else {
self.input + self.output
},
}
}
fn billable_total(self) -> u64 {
if self.total > 0 {
self.total
} else {
self.input.saturating_add(self.output)
}
}
fn advance(self, delta: RawUsage) -> Self {
Self {
input: self.input.saturating_add(delta.input),
cached_input: self.cached_input.saturating_add(delta.cached_input),
output: self.output.saturating_add(delta.output),
reasoning_output: self.reasoning_output.saturating_add(delta.reasoning_output),
total: self.total.saturating_add(delta.billable_total()),
}
}
}
impl UsagePayload {
pub(in crate::app) fn into_raw_usage(self) -> RawUsage {
RawUsage {
input: self.input_tokens,
cached_input: self
.cached_input_tokens
.or(self.cache_read_input_tokens)
.unwrap_or(0),
output: self.output_tokens,
reasoning_output: self.reasoning_output_tokens,
total: self.total_tokens,
}
}
}
#[derive(Clone, Debug, Default)]
pub(in crate::app) struct SessionParseCheckpoint {
pub(in crate::app) offset: u64,
pub(in crate::app) previous_totals: Option<RawUsage>,
pub(in crate::app) current_model: Option<String>,
pub(in crate::app) current_model_is_fallback: bool,
}
#[derive(Clone, Debug)]
pub(in crate::app) struct TokenUsageEvent<'session, 'model> {
pub(in crate::app) session_key: &'session str,
pub(in crate::app) session_id: &'session str,
pub(in crate::app) timestamp_utc: DateTime<Utc>,
pub(in crate::app) model: &'model str,
pub(in crate::app) is_fallback_model: bool,
pub(in crate::app) usage: UsageTotals,
}
#[derive(Deserialize)]
struct SessionLogEntry<'a> {
#[serde(
rename = "type",
borrow,
default,
deserialize_with = "deserialize_optional_cow_lossy"
)]
entry_type: Option<Cow<'a, str>>,
#[serde(borrow, default, deserialize_with = "deserialize_optional_cow_lossy")]
timestamp: Option<Cow<'a, str>>,
#[serde(
borrow,
default,
deserialize_with = "deserialize_optional_object_lossy"
)]
payload: Option<EntryPayload<'a>>,
}
#[derive(Default, Deserialize)]
pub(in crate::app) struct EntryPayload<'a> {
#[serde(
rename = "type",
borrow,
default,
deserialize_with = "deserialize_optional_cow_lossy"
)]
payload_type: Option<Cow<'a, str>>,
#[serde(
borrow,
default,
deserialize_with = "deserialize_optional_object_lossy"
)]
info: Option<EntryInfo<'a>>,
#[serde(default, deserialize_with = "deserialize_optional_object_lossy")]
last_token_usage: Option<UsagePayload>,
#[serde(default, deserialize_with = "deserialize_optional_object_lossy")]
total_token_usage: Option<UsagePayload>,
#[serde(flatten, borrow)]
model_fields: ModelFields<'a>,
}
#[derive(Default, Deserialize)]
struct EntryInfo<'a> {
#[serde(default, deserialize_with = "deserialize_optional_object_lossy")]
last_token_usage: Option<UsagePayload>,
#[serde(default, deserialize_with = "deserialize_optional_object_lossy")]
total_token_usage: Option<UsagePayload>,
#[serde(flatten, borrow)]
model_fields: ModelFields<'a>,
}
#[derive(Default, Deserialize)]
struct ModelFields<'a> {
#[serde(borrow, default, deserialize_with = "deserialize_optional_cow_lossy")]
model: Option<Cow<'a, str>>,
#[serde(
rename = "model_name",
borrow,
default,
deserialize_with = "deserialize_optional_cow_lossy"
)]
model_name: Option<Cow<'a, str>>,
#[serde(
borrow,
default,
deserialize_with = "deserialize_optional_object_lossy"
)]
metadata: Option<ModelMetadata<'a>>,
}
#[derive(Deserialize)]
struct ModelMetadata<'a> {
#[serde(borrow, default, deserialize_with = "deserialize_optional_cow_lossy")]
model: Option<Cow<'a, str>>,
}
pub(in crate::app) fn deserialize_optional_cow_lossy<'de, D>(
deserializer: D,
) -> std::result::Result<Option<Cow<'de, str>>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptionalCowVisitor;
impl<'de> serde::de::Visitor<'de> for OptionalCowVisitor {
type Value = Option<Cow<'de, str>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an optional string")
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_optional_cow_lossy(deserializer)
}
fn visit_borrowed_str<E>(self, value: &'de str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(Cow::Borrowed(value)))
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(Cow::Owned(value.to_string())))
}
fn visit_string<E>(self, value: String) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(Cow::Owned(value)))
}
fn visit_bool<E>(self, _value: bool) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_i64<E>(self, _value: i64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_u64<E>(self, _value: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_f64<E>(self, _value: f64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(None)
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
while map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{}
Ok(None)
}
}
deserializer.deserialize_any(OptionalCowVisitor)
}
pub(in crate::app) fn deserialize_u64_lossy<'de, D>(
deserializer: D,
) -> std::result::Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LossyU64Visitor;
impl<'de> serde::de::Visitor<'de> for LossyU64Visitor {
type Value = u64;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an integer token count")
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_u64_lossy(deserializer)
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(value)
}
fn visit_i64<E>(self, _value: i64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_f64<E>(self, _value: f64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_bool<E>(self, _value: bool) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_borrowed_str<E>(self, _value: &'de str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_str<E>(self, _value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_string<E>(self, _value: String) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(0)
}
fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(0)
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
while map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{}
Ok(0)
}
}
deserializer.deserialize_any(LossyU64Visitor)
}
pub(in crate::app) fn deserialize_optional_u64_lossy<'de, D>(
deserializer: D,
) -> std::result::Result<Option<u64>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptionalU64Visitor;
impl<'de> serde::de::Visitor<'de> for OptionalU64Visitor {
type Value = Option<u64>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an optional integer token count")
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_optional_u64_lossy(deserializer)
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Some(value))
}
fn visit_i64<E>(self, _value: i64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_f64<E>(self, _value: f64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_bool<E>(self, _value: bool) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_borrowed_str<E>(self, _value: &'de str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_str<E>(self, _value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_string<E>(self, _value: String) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(None)
}
fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
while map
.next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
.is_some()
{}
Ok(None)
}
}
deserializer.deserialize_any(OptionalU64Visitor)
}
pub(in crate::app) fn deserialize_optional_object_lossy<'de, D, T>(
deserializer: D,
) -> std::result::Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
struct OptionalObjectVisitor<T>(PhantomData<T>);
impl<'de, T> serde::de::Visitor<'de> for OptionalObjectVisitor<T>
where
T: serde::Deserialize<'de>,
{
type Value = Option<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an optional object")
}
fn visit_none<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> std::result::Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserialize_optional_object_lossy(deserializer)
}
fn visit_map<A>(self, map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let value = T::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
Ok(Some(value))
}
fn visit_bool<E>(self, _value: bool) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_i64<E>(self, _value: i64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_u64<E>(self, _value: u64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_f64<E>(self, _value: f64) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_borrowed_str<E>(self, _value: &'de str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_str<E>(self, _value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_string<E>(self, _value: String) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
while sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {}
Ok(None)
}
}
deserializer.deserialize_any(OptionalObjectVisitor::<T>(PhantomData))
}
#[allow(
clippy::struct_field_names,
reason = "field names mirror the Codex JSON payload shape verbatim"
)]
#[derive(Clone, Copy, Debug, Default, Deserialize)]
pub(in crate::app) struct UsagePayload {
#[serde(default, deserialize_with = "deserialize_u64_lossy")]
input_tokens: u64,
#[serde(default, deserialize_with = "deserialize_optional_u64_lossy")]
cached_input_tokens: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64_lossy")]
cache_read_input_tokens: Option<u64>,
#[serde(default, deserialize_with = "deserialize_u64_lossy")]
output_tokens: u64,
#[serde(default, deserialize_with = "deserialize_u64_lossy")]
reasoning_output_tokens: u64,
#[serde(default, deserialize_with = "deserialize_u64_lossy")]
total_tokens: u64,
}
#[cfg(test)]
pub(in crate::app) fn scan_session_file_with(
file: &Path,
session_id: &str,
mut on_event: impl FnMut(&TokenUsageEvent<'_, '_>),
) -> Result<()> {
let _ = scan_session_file_from_checkpoint(
file,
session_id,
&SessionParseCheckpoint::default(),
|event| {
on_event(event);
},
)?;
Ok(())
}
pub(in crate::app) fn scan_session_file_with_callback_and_observer<O>(
file: &Path,
session_id: &str,
observer: &O,
mut on_event: impl FnMut(&TokenUsageEvent<'_, '_>),
) -> Result<()>
where
O: ScanObserver,
{
let _ = scan_session_file_from_checkpoint_with_observer(
file,
session_id,
&SessionParseCheckpoint::default(),
observer,
|event| on_event(event),
)?;
observer.on_file_complete();
Ok(())
}
pub(in crate::app) fn scan_session_file_from_checkpoint(
file: &Path,
session_id: &str,
checkpoint: &SessionParseCheckpoint,
mut on_event: impl FnMut(&TokenUsageEvent<'_, '_>),
) -> Result<SessionParseCheckpoint> {
scan_session_file_from_checkpoint_inner(
file,
session_id,
checkpoint,
|| {},
|event| {
on_event(event);
},
)
}
pub(in crate::app) fn scan_session_file_from_checkpoint_with_observer<O>(
file: &Path,
session_id: &str,
checkpoint: &SessionParseCheckpoint,
observer: &O,
mut on_event: impl FnMut(&TokenUsageEvent<'_, '_>),
) -> Result<SessionParseCheckpoint>
where
O: ScanObserver,
{
scan_session_file_from_checkpoint_inner(
file,
session_id,
checkpoint,
|| observer.before_file_open(),
|event| on_event(event),
)
}
fn scan_session_file_from_checkpoint_inner(
file: &Path,
session_id: &str,
checkpoint: &SessionParseCheckpoint,
before_file_open: impl FnOnce(),
mut on_event: impl FnMut(&TokenUsageEvent<'_, '_>),
) -> Result<SessionParseCheckpoint> {
before_file_open();
let mut file = File::open(file)?;
file.seek(SeekFrom::Start(checkpoint.offset))?;
let reader = BufReader::new(file);
let mut line = String::new();
let mut previous_totals = checkpoint.previous_totals;
let mut current_model = checkpoint.current_model.clone();
let mut current_model_is_fallback = checkpoint.current_model_is_fallback;
let mut reader = reader;
let mut offset = checkpoint.offset;
loop {
line.clear();
let line_start_offset = offset;
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break;
}
let next_offset = offset.saturating_add(u64::try_from(bytes_read).unwrap_or(u64::MAX));
let trimmed = line.trim();
if trimmed.is_empty() {
offset = next_offset;
continue;
}
if !line.ends_with('\n') && serde_json::from_str::<SessionLogEntry<'_>>(trimmed).is_err() {
offset = line_start_offset;
break;
}
if !line_might_affect_usage(trimmed) {
offset = next_offset;
continue;
}
if let Some(event) = parse_token_usage_line(
trimmed,
session_id,
session_id,
&mut previous_totals,
&mut current_model,
&mut current_model_is_fallback,
)? {
on_event(&event);
}
offset = next_offset;
}
Ok(SessionParseCheckpoint {
offset,
previous_totals,
current_model,
current_model_is_fallback,
})
}
pub(in crate::app) fn line_might_affect_usage(line: &str) -> bool {
line.contains("\\u") || line.contains("turn_context") || line.contains("token_count")
}
pub(in crate::app) fn parse_token_usage_line<'session, 'model>(
line: &str,
session_key: &'session str,
session_id: &'session str,
previous_totals: &mut Option<RawUsage>,
current_model: &'model mut Option<String>,
current_model_is_fallback: &mut bool,
) -> Result<Option<TokenUsageEvent<'session, 'model>>> {
let Ok(entry) = serde_json::from_str::<SessionLogEntry<'_>>(line) else {
return Ok(None);
};
let Some(entry_type) = entry.entry_type.as_deref() else {
return Ok(None);
};
if entry_type == "turn_context" {
if let Some(model) = entry.payload.as_ref().and_then(extract_payload_model) {
remember_model(current_model, model);
*current_model_is_fallback = false;
}
return Ok(None);
}
if entry_type != "event_msg" {
return Ok(None);
}
let Some(payload) = entry.payload.as_ref() else {
return Ok(None);
};
if payload.payload_type.as_deref() != Some("token_count") {
return Ok(None);
}
let Some(timestamp) = entry.timestamp.as_deref() else {
return Ok(None);
};
let Some(usage) = extract_event_usage(payload, previous_totals) else {
return Ok(None);
};
let (model, is_fallback_model) =
resolve_event_model(payload, current_model, current_model_is_fallback);
let timestamp_utc = DateTime::parse_from_rfc3339(timestamp)
.wrap_err_with(|| format!("invalid timestamp {timestamp}"))?
.with_timezone(&Utc);
Ok(Some(TokenUsageEvent {
session_key,
session_id,
timestamp_utc,
model,
is_fallback_model,
usage,
}))
}
fn extract_event_usage(
payload: &EntryPayload<'_>,
previous_totals: &mut Option<RawUsage>,
) -> Option<UsageTotals> {
let (last_usage, total_usage) = if payload.info.is_some() {
(
info_usage(payload, UsageKind::Last),
info_usage(payload, UsageKind::Total),
)
} else {
(
payload
.last_token_usage
.as_ref()
.copied()
.map(UsagePayload::into_raw_usage),
payload
.total_token_usage
.as_ref()
.copied()
.map(UsagePayload::into_raw_usage),
)
};
let mut raw_usage = last_usage;
if raw_usage.is_none()
&& let Some(total_usage) = total_usage
{
raw_usage = Some(subtract_usage(total_usage, *previous_totals));
}
if let Some(total_usage) = total_usage {
*previous_totals = Some(total_usage);
} else if let Some(last_usage) = last_usage {
*previous_totals = Some(previous_totals.unwrap_or_default().advance(last_usage));
}
let usage = raw_usage?.into_usage_totals();
if usage.input == 0
&& usage.cached_input == 0
&& usage.output == 0
&& usage.reasoning_output == 0
{
return None;
}
Some(usage)
}
fn resolve_event_model<'a>(
payload: &EntryPayload<'_>,
current_model: &'a mut Option<String>,
current_model_is_fallback: &mut bool,
) -> (&'a str, bool) {
if let Some(model) = extract_payload_model(payload) {
remember_model(current_model, model);
*current_model_is_fallback = false;
}
if current_model.is_none() {
remember_model(current_model, DEFAULT_FALLBACK_MODEL);
*current_model_is_fallback = true;
}
(
current_model
.as_deref()
.expect("resolved event model should always be present"),
*current_model_is_fallback,
)
}
pub(in crate::app) fn extract_payload_model<'a>(payload: &'a EntryPayload<'a>) -> Option<&'a str> {
let info_fields = payload.info.as_ref().map(|info| &info.model_fields);
[
info_fields.and_then(|fields| fields.model.as_deref()),
info_fields.and_then(|fields| fields.model_name.as_deref()),
payload.model_fields.model.as_deref(),
payload.model_fields.model_name.as_deref(),
info_fields.and_then(metadata_model),
metadata_model(&payload.model_fields),
]
.into_iter()
.flatten()
.map(str::trim)
.find(|model| !model.is_empty())
}
fn metadata_model<'a>(fields: &'a ModelFields<'a>) -> Option<&'a str> {
fields
.metadata
.as_ref()
.and_then(|metadata| metadata.model.as_deref())
}
fn remember_model(current_model: &mut Option<String>, model: &str) {
match current_model {
Some(current) => {
current.clear();
current.push_str(model);
}
None => *current_model = Some(model.to_string()),
}
}
#[derive(Clone, Copy)]
enum UsageKind {
Last,
Total,
}
fn info_usage(payload: &EntryPayload<'_>, usage_kind: UsageKind) -> Option<RawUsage> {
let info = payload.info.as_ref()?;
match usage_kind {
UsageKind::Last => info
.last_token_usage
.as_ref()
.copied()
.map(UsagePayload::into_raw_usage),
UsageKind::Total => info
.total_token_usage
.as_ref()
.copied()
.map(UsagePayload::into_raw_usage),
}
}
pub(in crate::app) fn subtract_usage(current: RawUsage, previous: Option<RawUsage>) -> RawUsage {
let previous = previous.unwrap_or_default();
RawUsage {
input: current.input.saturating_sub(previous.input),
cached_input: current.cached_input.saturating_sub(previous.cached_input),
output: current.output.saturating_sub(previous.output),
reasoning_output: current
.reasoning_output
.saturating_sub(previous.reasoning_output),
total: current.total.saturating_sub(previous.total),
}
}