agent-sessions 0.2.1

Stream Claude Code and Codex JSONL sessions as typed Rust events with source offsets
Documentation
use crate::{AccountingPolicy, LineErrorKind, TokenCounts};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum UsageAdjustment {
    NegativeClamped { field: String, original: i64 },
    CacheTtlCapped { original: u64, total: u64 },
    UnreportedCounters,
    MissingCumulativeBaseline,
    CumulativeRegression,
    UncertainCumulativeBaseline,
}

pub(crate) fn counts(
    v: &Value,
    claude: bool,
    policy: AccountingPolicy,
) -> Result<(TokenCounts, Vec<UsageAdjustment>), LineErrorKind> {
    let statistics = policy == AccountingPolicy::UsageStatistics;
    let mut corrected: Option<Value> = None;
    let mut adjustments = Vec::new();
    if statistics && claude {
        for key in [
            "input_tokens",
            "output_tokens",
            "cache_creation_input_tokens",
            "cache_read_input_tokens",
        ] {
            if let Some(original) = v.get(key).and_then(Value::as_i64).filter(|n| *n < 0) {
                corrected.get_or_insert_with(|| v.clone())[key] = Value::from(0);
                adjustments.push(UsageAdjustment::NegativeClamped {
                    field: key.into(),
                    original,
                });
            }
        }
        if let Some(original) = v
            .pointer("/cache_creation/ephemeral_1h_input_tokens")
            .and_then(Value::as_i64)
            .filter(|n| *n < 0)
        {
            corrected.get_or_insert_with(|| v.clone())["cache_creation"]["ephemeral_1h_input_tokens"] =
                Value::from(0);
            adjustments.push(UsageAdjustment::NegativeClamped {
                field: "ephemeral_1h_input_tokens".into(),
                original,
            });
        }
        let current = corrected.as_ref().unwrap_or(v);
        let total = current
            .get("cache_creation_input_tokens")
            .and_then(Value::as_u64)
            .unwrap_or(0);
        if let Some(original) = current
            .pointer("/cache_creation/ephemeral_1h_input_tokens")
            .and_then(Value::as_u64)
            .filter(|n| *n > total)
        {
            corrected.get_or_insert_with(|| v.clone())["cache_creation"]["ephemeral_1h_input_tokens"] =
                Value::from(total);
            adjustments.push(UsageAdjustment::CacheTtlCapped { original, total });
        }
    }
    let counts = crate::tokens::parse_counts(corrected.as_ref().unwrap_or(v), claude, statistics)?;
    if counts.is_missing() {
        adjustments.push(UsageAdjustment::UnreportedCounters);
    }
    Ok((counts, adjustments))
}