1pub const DEFAULT_CONTEXT_WINDOW: u64 = 128_000;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct ModelLimit {
24 pub context: u64,
26 pub output: u64,
28}
29
30pub const DEFAULT_RESERVE_CAP: u64 = 32_768;
34
35impl ModelLimit {
36 pub fn reserve(&self) -> u64 {
38 self.output.min(DEFAULT_RESERVE_CAP)
39 }
40
41 pub fn trigger(&self) -> u64 {
44 self.context.saturating_sub(self.reserve())
45 }
46
47 pub fn is_full(&self, input_tokens: u64) -> bool {
50 input_tokens >= self.trigger()
51 }
52}
53
54pub fn is_opencode_go(base_url: Option<&str>) -> bool {
56 base_url.is_some_and(|u| u.to_ascii_lowercase().contains("opencode.ai/zen/go"))
57}
58
59pub fn model_limit(base_url: Option<&str>, model: &str) -> Option<ModelLimit> {
62 if is_opencode_go(base_url) {
63 return opencode_go(model);
64 }
65 None
66}
67
68fn opencode_go(model: &str) -> Option<ModelLimit> {
69 OPENCODE_GO
70 .iter()
71 .find(|(id, ..)| *id == model)
72 .map(|(_, context, output)| ModelLimit {
73 context: *context,
74 output: *output,
75 })
76}
77
78static OPENCODE_GO: &[(&str, u64, u64)] = &[
81 ("claude-haiku-5-5", 1_000_000, 128_000),
82 ("deepseek-v4-flash", 1_000_000, 384_000),
83 ("deepseek-v4-flash-vision-exp", 1_000_000, 384_000),
84 ("deepseek-v4-pro", 1_000_000, 384_000),
85 ("deepseek-v4.1-flash", 1_000_000, 384_000),
86 ("glm-5", 202_752, 32_768),
87 ("glm-5.1", 202_752, 32_768),
88 ("glm-5.2", 1_000_000, 131_072),
89 ("glm-5.3", 1_000_000, 131_072),
90 ("glm-5.3-flash", 1_000_000, 131_072),
91 ("gpt-5.6-luna", 1_050_000, 128_000),
92 ("gpt-6-luna", 1_050_000, 128_000),
93 ("grok-4.5", 500_000, 500_000),
94 ("grok-4.6", 500_000, 500_000),
95 ("grok-4.7", 500_000, 500_000),
96 ("hy3", 256_000, 128_000),
97 ("hy4-preview", 1_024_000, 64_000),
98 ("kimi-k2.5", 262_144, 65_536),
99 ("kimi-k2.6", 262_144, 65_536),
100 ("kimi-k2.7-code", 262_144, 262_144),
101 ("kimi-k3", 1_048_576, 131_072),
102 ("longcat-2.0", 1_000_000, 131_072),
103 ("longcat-2.5-preview-free", 1_000_000, 131_072),
104 ("mimo-v2-omni", 262_144, 128_000),
105 ("mimo-v2-pro", 1_048_576, 128_000),
106 ("mimo-v2.5", 1_000_000, 128_000),
107 ("mimo-v2.5-pro", 1_048_576, 128_000),
108 ("mimo-v2.6-flash", 1_048_576, 131_072),
109 ("mimo-v2.6-pro", 1_048_576, 131_072),
110 ("minimax-m2.5", 204_800, 65_536),
111 ("minimax-m2.7", 204_800, 131_072),
112 ("minimax-m3", 1_000_000, 131_072),
113 ("muse-spark-1.2-contributor", 1_048_576, 131_072),
114 ("muse-spark-1.3-contributor", 1_048_576, 131_072),
115 ("omen-alpha", 500_000, 128_000),
116 ("ox-alpha-free", 1_000_000, 131_072),
117 ("qwen3.5-plus", 262_144, 65_536),
118 ("qwen3.6-plus", 1_000_000, 65_536),
119 ("qwen3.7-max", 1_000_000, 65_536),
120 ("qwen3.7-plus", 1_000_000, 65_536),
121 ("qwen3.8-flash", 1_000_000, 131_072),
122 ("qwen3.8-max", 1_000_000, 131_072),
123 ("space-bunny", 1_048_576, 524_288),
124 ("space-bunny-free", 1_048_576, 524_288),
125 ("step-5-preview-free", 1_000_000, 65_536),
126];
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::compaction::CompactionPolicy;
132
133 const ZEN: Option<&str> = Some("https://opencode.ai/zen/go/v1");
134
135 #[test]
136 fn recognizes_opencode_go_endpoint() {
137 assert!(is_opencode_go(ZEN));
138 assert!(is_opencode_go(Some("https://opencode.ai/zen/go/v1/")));
139 assert!(is_opencode_go(Some("HTTPS://OPENCODE.AI/ZEN/GO/V1")));
140 assert!(!is_opencode_go(Some("https://api.openai.com/v1")));
141 assert!(!is_opencode_go(Some("https://opencode.ai/zen/v1")));
142 assert!(!is_opencode_go(None));
143 }
144
145 #[test]
146 fn looks_up_configured_model() {
147 let l = model_limit(ZEN, "deepseek-v4.1-flash").expect("known opencode model");
148 assert_eq!(l.context, 1_000_000);
149 assert_eq!(l.output, 384_000);
150 }
151
152 #[test]
153 fn unknown_model_or_provider_is_none() {
154 assert_eq!(model_limit(ZEN, "no-such-model"), None);
155 assert_eq!(model_limit(Some("https://api.openai.com/v1"), "kimi-k3"), None);
157 assert_eq!(model_limit(None, "kimi-k3"), None);
158 }
159
160 #[test]
161 fn step_5_preview_free_is_a_1m_window() {
162 assert_eq!(
166 model_limit(ZEN, "step-5-preview-free"),
167 Some(ModelLimit {
168 context: 1_000_000,
169 output: 65_536,
170 })
171 );
172 }
173
174 #[test]
175 fn catalog_window_beats_the_default_in_compaction() {
176 let p = CompactionPolicy::default();
179 let window = p.context_window(ZEN, "step-5-preview-free");
180 assert_eq!(window, 1_000_000);
181 assert!(
182 !p.should_compact(window, 111_616),
183 "the old 128k trigger must not fire on a 1M model"
184 );
185 assert!(
186 p.should_compact(window, 983_616),
187 "1M window - 16_384 reserved = 983,616"
188 );
189 }
190
191 #[test]
192 fn configured_window_still_wins_only_when_the_catalog_misses() {
193 let p = CompactionPolicy {
194 window: Some(200_000),
195 ..Default::default()
196 };
197 assert_eq!(p.context_window(ZEN, "not-a-real-model"), 200_000);
199 assert_eq!(p.context_window(ZEN, "step-5-preview-free"), 1_000_000);
201 }
202
203 #[test]
204 fn reserve_is_capped_and_trigger_and_is_full_agree() {
205 let big = ModelLimit { context: 1_000_000, output: 384_000 };
207 assert_eq!(big.reserve(), DEFAULT_RESERVE_CAP);
208 assert_eq!(big.trigger(), 1_000_000 - DEFAULT_RESERVE_CAP);
209 assert!(!big.is_full(big.trigger() - 1));
210 assert!(big.is_full(big.trigger()));
211
212 let small = ModelLimit { context: 8_192, output: 4_096 };
214 assert_eq!(small.reserve(), 4_096);
215 assert_eq!(small.trigger(), 4_096);
216 assert!(small.is_full(4_096));
217 }
218}