use serde::Deserialize;
use std::collections::BTreeMap;
const RAW: &str = cmduse_core::ZEN_JSON;
pub const DEFAULT_PLAN: &str = "go";
#[derive(Debug, Clone, Deserialize)]
pub struct RateVariant {
pub peak: Option<String>,
#[serde(rename = "tierMaxTokens")]
pub tier_max_tokens: Option<u64>,
#[serde(rename = "tierMinTokens")]
pub tier_min_tokens: Option<u64>,
pub input: f64,
pub output: f64,
#[serde(rename = "cacheRead")]
pub cache_read: f64,
#[serde(rename = "cacheWrite")]
pub cache_write: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Model {
pub id: String,
pub name: String,
pub variants: Vec<RateVariant>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WindowShare {
#[serde(rename = "fiveHour")]
pub five_hour: f64,
pub weekly: f64,
pub monthly: f64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GoPlan {
pub label: String,
#[serde(rename = "priceUsd")]
pub price_usd: f64,
#[serde(rename = "allowanceByModel")]
pub allowance_by_model: BTreeMap<String, f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Go {
#[serde(rename = "windowShare")]
pub window_share: WindowShare,
pub models: Vec<Model>,
pub plans: BTreeMap<String, GoPlan>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Zen {
pub models: Vec<Model>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Catalog {
#[serde(rename = "extractedAt")]
pub extracted_at: String,
pub go: Go,
pub zen: Zen,
}
pub fn catalog() -> Catalog {
serde_json::from_str(RAW)
.expect("core/zen.json is generated; regenerate with scripts/extract-zen.ts")
}
impl Catalog {
pub fn plan_ids(&self) -> Vec<&str> {
self.go.plans.keys().map(String::as_str).collect()
}
pub fn plan(&self, plan_id: &str) -> Option<&GoPlan> {
self.go.plans.get(plan_id)
}
pub fn go_limit(&self, plan_id: &str, model_id: &str) -> Option<f64> {
self.plan(plan_id)
.and_then(|plan| plan.allowance_by_model.get(model_id).copied())
}
}
pub fn window_cap(limit: f64, share: f64) -> f64 {
limit * share
}
pub fn is_peak(at_ms: i64) -> bool {
let secs = at_ms.div_euclid(1000);
let weekday = (secs.div_euclid(86_400) + 4).rem_euclid(7);
if weekday == 0 || weekday == 6 {
return false;
}
let hour = secs.rem_euclid(86_400) / 3600;
(1..4).contains(&hour) || (6..10).contains(&hour)
}
impl Catalog {
#[allow(clippy::too_many_arguments)]
pub fn price(
&self,
model_id: &str,
at_ms: i64,
input: u64,
output: u64,
reasoning: u64,
cache_read: u64,
cache_write: u64,
) -> Option<f64> {
let model = self.go.models.iter().find(|m| m.id == model_id)?;
let peak = model
.variants
.iter()
.find(|v| v.peak.as_deref() == Some("peak"));
let off_peak = model
.variants
.iter()
.find(|v| v.peak.as_deref() == Some("off-peak"));
let rates = match (peak, off_peak) {
(Some(peak), Some(_)) if is_peak(at_ms) => peak,
(Some(_), Some(off)) => off,
_ => model.variants.first()?,
};
let cost = input as f64 * rates.input
+ (output + reasoning) as f64 * rates.output
+ cache_read as f64 * rates.cache_read
+ cache_write as f64 * rates.cache_write;
(cost > 0.0).then_some(cost / 1e6)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peak_window_is_weekday_utc_hours_only() {
let at = |s: &str| cmduse_core::dates::parse_iso_utc(s).expect(s) as i64;
assert!(is_peak(at("2026-10-16T02:00:00Z")));
assert!(is_peak(at("2026-10-16T09:59:00Z")));
assert!(!is_peak(at("2026-10-16T10:00:00Z")));
assert!(!is_peak(at("2026-10-16T05:00:00Z")), "gap between windows");
assert!(!is_peak(at("2026-10-16T00:59:00Z")));
assert!(!is_peak(at("2026-10-17T02:00:00Z")), "Saturday");
assert!(!is_peak(at("2026-10-18T02:00:00Z")), "Sunday");
assert!(is_peak(at("2026-10-19T02:00:00Z")));
let c = catalog();
let flash = |ts: &str| c.price("deepseek-v4.1-flash", at(ts), 1_000_000, 0, 0, 0, 0);
assert_eq!(flash("2026-10-16T02:00:00Z"), Some(0.3));
assert_eq!(flash("2026-10-18T02:00:00Z"), Some(0.15));
}
#[test]
fn catalogue_parses_and_carries_both_plans() {
let c = catalog();
assert!(c.go.models.len() > 20, "go models: {}", c.go.models.len());
assert!(!c.zen.models.is_empty());
assert_eq!(c.plan_ids(), vec!["go", "go-plus"]);
assert_eq!(c.plan("go").map(|p| p.price_usd), Some(10.0));
assert_eq!(c.plan("go-plus").map(|p| p.price_usd), Some(40.0));
assert_eq!(c.go_limit("go", "glm-5.3-flash"), Some(60.0));
assert_eq!(c.go_limit("go-plus", "glm-5.3-flash"), Some(180.0));
assert_eq!(c.go_limit("go", "nope-not-a-model"), None);
assert_eq!(c.go_limit("nope-not-a-plan", "glm-5.3-flash"), None);
}
#[test]
fn plans_share_the_model_set_and_differ_on_allowance() {
let c = catalog();
let go = c.plan("go").expect("go");
let plus = c.plan("go-plus").expect("go-plus");
assert_eq!(go.label, "Go");
assert_eq!(plus.label, "Go Plus");
assert_eq!(go.allowance_by_model.len(), plus.allowance_by_model.len());
assert!(plus
.allowance_by_model
.iter()
.all(|(id, _)| go.allowance_by_model.contains_key(id)));
assert!(plus
.allowance_by_model
.iter()
.any(|(id, limit)| go.allowance_by_model.get(id).is_some_and(|g| limit > g)));
}
#[test]
fn window_split_matches_the_docs() {
let c = catalog();
assert_eq!(c.go.window_share.five_hour, 0.2);
assert_eq!(c.go.window_share.weekly, 0.5);
assert_eq!(c.go.window_share.monthly, 1.0);
assert_eq!(window_cap(60.0, c.go.window_share.five_hour), 12.0);
}
#[test]
fn tiered_and_peak_variants_are_kept() {
let c = catalog();
let qwen =
c.go.models
.iter()
.find(|m| m.id == "qwen3.7-plus")
.expect("qwen3.7-plus");
assert_eq!(qwen.variants.len(), 2);
assert!(qwen
.variants
.iter()
.any(|v| v.tier_max_tokens == Some(256_000)));
let deepseek =
c.go.models
.iter()
.find(|m| m.id == "deepseek-v4-flash")
.expect("deepseek");
assert!(deepseek
.variants
.iter()
.any(|v| v.peak.as_deref() == Some("peak")));
}
}