pub mod claude;
pub mod codex;
pub mod dsh;
pub mod gemini;
pub mod grok;
pub mod kimi;
pub mod opencode;
pub mod pi;
pub mod prince;
use crate::error::AppError;
use crate::model::{AppKind, UsageEntry};
use serde_json::Value;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
pub fn fresh_input(input: u64, cache_read: u64, cache_creation: u64) -> u64 {
input
.checked_sub(cache_read.saturating_add(cache_creation))
.unwrap_or(input)
}
pub fn strip_provider(model: &str) -> &str {
match model.rfind('/') {
Some(pos) if pos + 1 < model.len() => &model[pos + 1..],
_ => model,
}
}
pub fn normalize_model(raw: &str) -> String {
let model = strip_provider(raw.trim()).to_ascii_lowercase();
if model.is_empty() || model.ends_with('/') {
"unknown".to_string()
} else {
model
}
}
pub fn value_hash(value: &Value) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
pub fn collect(apps: &[AppKind], threads: Option<usize>) -> Result<Vec<UsageEntry>, AppError> {
let mut entries = Vec::new();
for &app in apps {
match app {
AppKind::Claude => entries.extend(claude::collect(threads)?),
AppKind::Codex => entries.extend(codex::collect(threads)?),
AppKind::OpenCode => entries.extend(opencode::collect()?),
AppKind::Gemini => entries.extend(gemini::collect(threads)?),
AppKind::Grok => entries.extend(grok::collect(threads)?),
AppKind::Pi => entries.extend(pi::collect(threads)?),
AppKind::Kimi => entries.extend(kimi::collect(threads)?),
AppKind::Dsh => entries.extend(dsh::collect(threads)?),
}
}
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::{fresh_input, normalize_model, strip_provider};
#[test]
fn test_fresh_input() {
assert_eq!(fresh_input(100, 50, 0), 50);
assert_eq!(fresh_input(100, 50, 20), 30);
assert_eq!(fresh_input(50, 0, 0), 50);
assert_eq!(fresh_input(10, 20, 0), 10); }
#[test]
fn test_strip_provider() {
assert_eq!(strip_provider("moonshot-cn/kimi-k3"), "kimi-k3");
assert_eq!(strip_provider("a/b/kimi-k3"), "kimi-k3"); assert_eq!(strip_provider("kimi-k3"), "kimi-k3"); assert_eq!(strip_provider("moonshot-cn/"), "moonshot-cn/"); }
#[test]
fn test_normalize_model() {
assert_eq!(
normalize_model(" OpenAI/GPT-5.1-2025-01-01 "),
"gpt-5.1-2025-01-01"
);
assert_eq!(normalize_model("moonshot-cn/kimi-k3"), "kimi-k3");
assert_eq!(
normalize_model("a/b/Claude-Sonnet-4-5"),
"claude-sonnet-4-5"
); assert_eq!(normalize_model("gpt-5"), "gpt-5"); assert_eq!(
normalize_model("claude-sonnet-4-5-20250929"),
"claude-sonnet-4-5-20250929"
);
assert_eq!(normalize_model(""), "unknown"); assert_eq!(normalize_model(" "), "unknown"); assert_eq!(normalize_model("moonshot-cn/"), "unknown"); }
}