csusage-core 0.0.1

Coding agent CLI usage reports (ccusage fork with Claude Science support)
use serde_json::Value;

use crate::TokenUsageRaw;

pub fn json_value_u64(value: Option<&Value>) -> u64 {
    value.and_then(Value::as_u64).unwrap_or_default()
}

pub fn non_empty_json_string(value: Option<&Value>) -> Option<String> {
    let value = value?.as_str()?.trim();
    (!value.is_empty()).then(|| value.to_string())
}

pub fn total_usage_tokens(usage: TokenUsageRaw) -> u64 {
    usage
        .input_tokens
        .saturating_add(usage.output_tokens)
        .saturating_add(usage.cache_creation_token_count())
        .saturating_add(usage.cache_read_input_tokens)
}

pub fn apply_total_token_fallback(
    mut usage: TokenUsageRaw,
    mut extra_total_tokens: u64,
    total_tokens: u64,
) -> (TokenUsageRaw, u64) {
    let known_tokens = total_usage_tokens(usage).saturating_add(extra_total_tokens);
    let missing_tokens = total_tokens.saturating_sub(known_tokens);
    if missing_tokens == 0 {
        return (usage, extra_total_tokens);
    }
    if usage.output_tokens == 0 {
        usage.output_tokens = missing_tokens;
    } else {
        extra_total_tokens = extra_total_tokens.saturating_add(missing_tokens);
    }
    (usage, extra_total_tokens)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn applies_total_token_fallback_to_missing_output_tokens() {
        let (usage, extra_total_tokens) = apply_total_token_fallback(
            TokenUsageRaw {
                input_tokens: 100,
                output_tokens: 0,
                cache_creation_input_tokens: 0,
                cache_read_input_tokens: 25,
                speed: None,
                cache_creation: None,
            },
            0,
            175,
        );

        assert_eq!(usage.input_tokens, 100);
        assert_eq!(usage.output_tokens, 50);
        assert_eq!(usage.cache_read_input_tokens, 25);
        assert_eq!(extra_total_tokens, 0);
    }

    #[test]
    fn keeps_total_fallback_as_extra_when_output_is_known() {
        let (usage, extra_total_tokens) = apply_total_token_fallback(
            TokenUsageRaw {
                input_tokens: 100,
                output_tokens: 50,
                cache_creation_input_tokens: 0,
                cache_read_input_tokens: 25,
                speed: None,
                cache_creation: None,
            },
            0,
            200,
        );

        assert_eq!(usage.output_tokens, 50);
        assert_eq!(extra_total_tokens, 25);
    }

    #[test]
    fn saturates_total_token_fallback_for_huge_counters() {
        let (usage, extra_total_tokens) = apply_total_token_fallback(
            TokenUsageRaw {
                input_tokens: u64::MAX,
                output_tokens: u64::MAX,
                cache_creation_input_tokens: 0,
                cache_read_input_tokens: u64::MAX,
                speed: None,
                cache_creation: Some(crate::CacheCreationRaw {
                    ephemeral_5m_input_tokens: u64::MAX,
                    ephemeral_1h_input_tokens: u64::MAX,
                }),
            },
            u64::MAX,
            u64::MAX,
        );

        assert_eq!(total_usage_tokens(usage), u64::MAX);
        assert_eq!(extra_total_tokens, u64::MAX);
    }
}