1use serde::Deserialize;
13use std::collections::BTreeMap;
14
15const RAW: &str = cmduse_core::ZEN_JSON;
18
19pub const DEFAULT_PLAN: &str = "go";
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct RateVariant {
24 pub peak: Option<String>,
25 #[serde(rename = "tierMaxTokens")]
26 pub tier_max_tokens: Option<u64>,
27 #[serde(rename = "tierMinTokens")]
28 pub tier_min_tokens: Option<u64>,
29 pub input: f64,
30 pub output: f64,
31 #[serde(rename = "cacheRead")]
32 pub cache_read: f64,
33 #[serde(rename = "cacheWrite")]
34 pub cache_write: f64,
35}
36
37#[derive(Debug, Clone, Deserialize)]
38pub struct Model {
39 pub id: String,
40 pub name: String,
41 pub variants: Vec<RateVariant>,
42}
43
44#[derive(Debug, Clone, Deserialize)]
45pub struct WindowShare {
46 #[serde(rename = "fiveHour")]
47 pub five_hour: f64,
48 pub weekly: f64,
49 pub monthly: f64,
50}
51
52#[derive(Debug, Clone, Deserialize)]
54pub struct GoPlan {
55 pub label: String,
56 #[serde(rename = "priceUsd")]
57 pub price_usd: f64,
58 #[serde(rename = "allowanceByModel")]
60 pub allowance_by_model: BTreeMap<String, f64>,
61}
62
63#[derive(Debug, Clone, Deserialize)]
64pub struct Go {
65 #[serde(rename = "windowShare")]
66 pub window_share: WindowShare,
67 pub models: Vec<Model>,
69 pub plans: BTreeMap<String, GoPlan>,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct Zen {
75 pub models: Vec<Model>,
76}
77
78#[derive(Debug, Clone, Deserialize)]
79pub struct Catalog {
80 #[serde(rename = "extractedAt")]
81 pub extracted_at: String,
82 pub go: Go,
83 pub zen: Zen,
84}
85
86pub fn catalog() -> Catalog {
88 serde_json::from_str(RAW)
89 .expect("core/zen.json is generated; regenerate with scripts/extract-zen.ts")
90}
91
92impl Catalog {
93 pub fn plan_ids(&self) -> Vec<&str> {
96 self.go.plans.keys().map(String::as_str).collect()
97 }
98
99 pub fn plan(&self, plan_id: &str) -> Option<&GoPlan> {
100 self.go.plans.get(plan_id)
101 }
102
103 pub fn go_limit(&self, plan_id: &str, model_id: &str) -> Option<f64> {
106 self.plan(plan_id)
107 .and_then(|plan| plan.allowance_by_model.get(model_id).copied())
108 }
109}
110
111pub fn window_cap(limit: f64, share: f64) -> f64 {
113 limit * share
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn catalogue_parses_and_carries_both_plans() {
122 let c = catalog();
123 assert!(c.go.models.len() > 20, "go models: {}", c.go.models.len());
124 assert!(!c.zen.models.is_empty());
125 assert_eq!(c.plan_ids(), vec!["go", "go-plus"]);
126 assert_eq!(c.plan("go").map(|p| p.price_usd), Some(10.0));
127 assert_eq!(c.plan("go-plus").map(|p| p.price_usd), Some(40.0));
128 assert_eq!(c.go_limit("go", "glm-5.3-flash"), Some(60.0));
129 assert_eq!(c.go_limit("go-plus", "glm-5.3-flash"), Some(180.0));
130 assert_eq!(c.go_limit("go", "nope-not-a-model"), None);
131 assert_eq!(c.go_limit("nope-not-a-plan", "glm-5.3-flash"), None);
132 }
133
134 #[test]
135 fn plans_share_the_model_set_and_differ_on_allowance() {
136 let c = catalog();
137 let go = c.plan("go").expect("go");
138 let plus = c.plan("go-plus").expect("go-plus");
139 assert_eq!(go.label, "Go");
140 assert_eq!(plus.label, "Go Plus");
141 assert_eq!(go.allowance_by_model.len(), plus.allowance_by_model.len());
142 assert!(plus
145 .allowance_by_model
146 .iter()
147 .all(|(id, _)| go.allowance_by_model.contains_key(id)));
148 assert!(plus
149 .allowance_by_model
150 .iter()
151 .any(|(id, limit)| go.allowance_by_model.get(id).is_some_and(|g| limit > g)));
152 }
153
154 #[test]
155 fn window_split_matches_the_docs() {
156 let c = catalog();
157 assert_eq!(c.go.window_share.five_hour, 0.2);
158 assert_eq!(c.go.window_share.weekly, 0.5);
159 assert_eq!(c.go.window_share.monthly, 1.0);
160 assert_eq!(window_cap(60.0, c.go.window_share.five_hour), 12.0);
161 }
162
163 #[test]
164 fn tiered_and_peak_variants_are_kept() {
165 let c = catalog();
166 let qwen =
167 c.go.models
168 .iter()
169 .find(|m| m.id == "qwen3.7-plus")
170 .expect("qwen3.7-plus");
171 assert_eq!(qwen.variants.len(), 2);
172 assert!(qwen
173 .variants
174 .iter()
175 .any(|v| v.tier_max_tokens == Some(256_000)));
176 let deepseek =
177 c.go.models
178 .iter()
179 .find(|m| m.id == "deepseek-v4-flash")
180 .expect("deepseek");
181 assert!(deepseek
182 .variants
183 .iter()
184 .any(|v| v.peak.as_deref() == Some("peak")));
185 }
186}