llm-tokei 0.1.6

Token usage stats CLI for Codex and OpenCode sessions
Documentation
use crate::aggregate::{Aggregate, GroupDim};
use crate::cli::Unit;
use serde::Serialize;
use std::collections::BTreeMap;

#[derive(Serialize)]
struct JsonRow<'a> {
  keys: serde_json::Map<String, serde_json::Value>,
  input: serde_json::Value,
  output: serde_json::Value,
  input_estimated: bool,
  output_estimated: bool,
  reasoning: serde_json::Value,
  cache_read: serde_json::Value,
  cache_write: serde_json::Value,
  total: serde_json::Value,
  calls: u64,
  rounds: u64,
  sessions: u64,
  cost_embedded: f64,
  cost: f64,
  #[serde(skip_serializing_if = "BTreeMap::is_empty")]
  cost_per: &'a BTreeMap<String, f64>,
  first_ts: Option<&'a chrono::DateTime<chrono::Utc>>,
  last_ts: Option<&'a chrono::DateTime<chrono::Utc>>,
}

pub fn render_json(aggs: &[Aggregate], dims: &[GroupDim], unit: Unit) -> String {
  let rows: Vec<JsonRow> = aggs
    .iter()
    .map(|a| {
      let mut keys = serde_json::Map::new();
      for (i, d) in dims.iter().enumerate() {
        keys.insert(
          d.label().to_string(),
          serde_json::Value::String(a.keys.get(i).cloned().unwrap_or_default()),
        );
      }
      JsonRow {
        keys,
        input: json_input(a, unit),
        output: json_output(a, unit),
        input_estimated: if unit == Unit::Bytes {
          a.input_bytes_estimated
        } else {
          a.input_estimated
        },
        output_estimated: if unit == Unit::Bytes {
          a.output_bytes_estimated
        } else {
          a.output_estimated
        },
        reasoning: json_reasoning(a, unit),
        cache_read: json_cache_read(a, unit),
        cache_write: json_cache_write(a, unit),
        total: json_total(a, unit),
        calls: a.calls,
        rounds: a.rounds,
        sessions: a.sessions,
        cost_embedded: a.cost_embedded,
        cost: a.cost,
        cost_per: &a.cost_per,
        first_ts: a.first_ts.as_ref(),
        last_ts: a.last_ts.as_ref(),
      }
    })
    .collect();
  serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".into())
}

fn json_input(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Tokens => serde_json::Value::from(a.input),
    Unit::Bytes => serde_json::Value::from(a.input_bytes),
    Unit::Cost => serde_json::Value::from(a.prompt_cost + a.cache_read_cost + a.cache_write_cost),
  }
}

fn json_output(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Tokens => serde_json::Value::from(a.output),
    Unit::Bytes => serde_json::Value::from(a.output_bytes),
    Unit::Cost => serde_json::Value::from(a.completion_cost + a.reasoning_cost),
  }
}

fn json_reasoning(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Cost => serde_json::Value::from(a.reasoning_cost),
    _ => serde_json::Value::from(a.reasoning),
  }
}

fn json_cache_read(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Cost => serde_json::Value::from(a.cache_read_cost),
    _ => serde_json::Value::from(a.cache_read),
  }
}

fn json_cache_write(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Cost => serde_json::Value::from(a.cache_write_cost),
    _ => serde_json::Value::from(a.cache_write),
  }
}

fn json_total(a: &Aggregate, unit: Unit) -> serde_json::Value {
  match unit {
    Unit::Cost => serde_json::Value::from(a.cost),
    _ => serde_json::Value::from(a.total),
  }
}