1use serde::Deserialize;
8
9const RAW: &str = cmduse_core::ZEN_JSON;
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct RateVariant {
15 pub peak: Option<String>,
16 #[serde(rename = "tierMaxTokens")]
17 pub tier_max_tokens: Option<u64>,
18 #[serde(rename = "tierMinTokens")]
19 pub tier_min_tokens: Option<u64>,
20 pub input: f64,
21 pub output: f64,
22 #[serde(rename = "cacheRead")]
23 pub cache_read: f64,
24 #[serde(rename = "cacheWrite")]
25 pub cache_write: f64,
26}
27
28#[derive(Debug, Clone, Deserialize)]
29pub struct Model {
30 pub id: String,
31 pub name: String,
32 #[serde(rename = "monthlyLimit")]
33 pub monthly_limit: Option<f64>,
34 pub variants: Vec<RateVariant>,
35}
36
37#[derive(Debug, Clone, Deserialize)]
38pub struct WindowShare {
39 #[serde(rename = "fiveHour")]
40 pub five_hour: f64,
41 pub weekly: f64,
42 pub monthly: f64,
43}
44
45#[derive(Debug, Clone, Deserialize)]
46pub struct Plans {
47 #[serde(rename = "priceUsd")]
48 pub price_usd: f64,
49 #[serde(rename = "windowShare")]
50 pub window_share: WindowShare,
51 pub models: Vec<Model>,
52}
53
54#[derive(Debug, Clone, Deserialize)]
55pub struct Zen {
56 pub models: Vec<Model>,
57}
58
59#[derive(Debug, Clone, Deserialize)]
60pub struct Catalog {
61 #[serde(rename = "extractedAt")]
62 pub extracted_at: String,
63 pub go: Plans,
64 pub zen: Zen,
65}
66
67pub fn catalog() -> Catalog {
69 serde_json::from_str(RAW)
70 .expect("core/zen.json is generated; regenerate with scripts/extract-zen.ts")
71}
72
73impl Catalog {
74 pub fn go_limit(&self, model_id: &str) -> Option<f64> {
76 self.go
77 .models
78 .iter()
79 .find(|m| m.id == model_id)
80 .and_then(|m| m.monthly_limit)
81 }
82}
83
84pub fn window_cap(limit: f64, share: f64) -> f64 {
86 limit * share
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn catalogue_parses_and_carries_limits() {
95 let c = catalog();
96 assert!(c.go.models.len() > 20, "go models: {}", c.go.models.len());
97 assert!(!c.zen.models.is_empty());
98 assert_eq!(c.go_limit("deepseek-v4.1-flash"), Some(60.0));
99 assert_eq!(c.go_limit("nope-not-a-model"), None);
100 }
101
102 #[test]
103 fn window_split_matches_the_docs() {
104 let c = catalog();
105 assert_eq!(c.go.window_share.five_hour, 0.2);
106 assert_eq!(c.go.window_share.weekly, 0.5);
107 assert_eq!(c.go.window_share.monthly, 1.0);
108 assert_eq!(window_cap(60.0, c.go.window_share.five_hour), 12.0);
109 }
110
111 #[test]
112 fn tiered_and_peak_variants_are_kept() {
113 let c = catalog();
114 let qwen =
115 c.go.models
116 .iter()
117 .find(|m| m.id == "qwen3.7-plus")
118 .expect("qwen3.7-plus");
119 assert_eq!(qwen.variants.len(), 2);
120 assert!(qwen
121 .variants
122 .iter()
123 .any(|v| v.tier_max_tokens == Some(256_000)));
124 let deepseek =
125 c.go.models
126 .iter()
127 .find(|m| m.id == "deepseek-v4-flash")
128 .expect("deepseek");
129 assert!(deepseek
130 .variants
131 .iter()
132 .any(|v| v.peak.as_deref() == Some("peak")));
133 }
134}