1use crate::streamfn::LlmEndpoint;
18
19pub const DEFAULT_CONTEXT_WINDOW: u64 = 128_000;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct ModelLimit {
26 pub context: u64,
28 pub output: u64,
30}
31
32pub const DEFAULT_RESERVE_CAP: u64 = 32_768;
36
37impl ModelLimit {
38 pub fn reserve(&self) -> u64 {
40 self.output.min(DEFAULT_RESERVE_CAP)
41 }
42
43 pub fn trigger(&self) -> u64 {
46 self.context.saturating_sub(self.reserve())
47 }
48
49 pub fn is_full(&self, input_tokens: u64) -> bool {
52 input_tokens >= self.trigger()
53 }
54}
55
56pub fn is_opencode_go(base_url: Option<&str>) -> bool {
58 base_url.is_some_and(|u| u.to_ascii_lowercase().contains("opencode.ai/zen/go"))
59}
60
61pub fn model_limit(base_url: Option<&str>, model: &str) -> Option<ModelLimit> {
64 if is_opencode_go(base_url) {
65 return opencode_go(model);
66 }
67 None
68}
69
70pub fn model_protocol(base_url: Option<&str>, model: &str) -> Option<LlmEndpoint> {
87 if is_opencode_go(base_url) && OPENCODE_GO_RESPONSES_ONLY.contains(&model) {
88 return Some(LlmEndpoint::Responses);
89 }
90 None
91}
92
93fn opencode_go(model: &str) -> Option<ModelLimit> {
94 OPENCODE_GO
95 .iter()
96 .find(|(id, ..)| *id == model)
97 .map(|(_, context, output)| ModelLimit {
98 context: *context,
99 output: *output,
100 })
101}
102
103static OPENCODE_GO_RESPONSES_ONLY: &[&str] = &[
109 "grok-4.6",
110 "grok-4.7",
111 "gpt-5.6-luna",
112 "gpt-6-luna",
113 "muse-spark-1.2-contributor",
114 "muse-spark-1.3-contributor",
115];
116
117static OPENCODE_GO: &[(&str, u64, u64)] = &[
120 ("claude-haiku-5-5", 1_000_000, 128_000),
121 ("deepseek-v4-flash", 1_000_000, 384_000),
122 ("deepseek-v4-flash-vision-exp", 1_000_000, 384_000),
123 ("deepseek-v4-pro", 1_000_000, 384_000),
124 ("deepseek-v4.1-flash", 1_000_000, 384_000),
125 ("glm-5", 202_752, 32_768),
126 ("glm-5.1", 202_752, 32_768),
127 ("glm-5.2", 1_000_000, 131_072),
128 ("glm-5.3", 1_000_000, 131_072),
129 ("glm-5.3-flash", 1_000_000, 131_072),
130 ("gpt-5.6-luna", 1_050_000, 128_000),
131 ("gpt-6-luna", 1_050_000, 128_000),
132 ("grok-4.5", 500_000, 500_000),
133 ("grok-4.6", 500_000, 500_000),
134 ("grok-4.7", 500_000, 500_000),
135 ("hy3", 256_000, 128_000),
136 ("hy4-preview", 1_024_000, 64_000),
137 ("kimi-k2.5", 262_144, 65_536),
138 ("kimi-k2.6", 262_144, 65_536),
139 ("kimi-k2.7-code", 262_144, 262_144),
140 ("kimi-k3", 1_048_576, 131_072),
141 ("longcat-2.0", 1_000_000, 131_072),
142 ("longcat-2.5-preview-free", 1_000_000, 131_072),
143 ("mimo-v2-omni", 262_144, 128_000),
144 ("mimo-v2-pro", 1_048_576, 128_000),
145 ("mimo-v2.5", 1_000_000, 128_000),
146 ("mimo-v2.5-pro", 1_048_576, 128_000),
147 ("mimo-v2.6-flash", 1_048_576, 131_072),
148 ("mimo-v2.6-pro", 1_048_576, 131_072),
149 ("minimax-m2.5", 204_800, 65_536),
150 ("minimax-m2.7", 204_800, 131_072),
151 ("minimax-m3", 1_000_000, 131_072),
152 ("muse-spark-1.2-contributor", 1_048_576, 131_072),
153 ("muse-spark-1.3-contributor", 1_048_576, 131_072),
154 ("omen-alpha", 500_000, 128_000),
155 ("ox-alpha-free", 1_000_000, 131_072),
156 ("qwen3.5-plus", 262_144, 65_536),
157 ("qwen3.6-plus", 1_000_000, 65_536),
158 ("qwen3.7-max", 1_000_000, 65_536),
159 ("qwen3.7-plus", 1_000_000, 65_536),
160 ("qwen3.8-flash", 1_000_000, 131_072),
161 ("qwen3.8-max", 1_000_000, 131_072),
162 ("space-bunny", 1_048_576, 524_288),
163 ("space-bunny-free", 1_048_576, 524_288),
164 ("step-5-preview-free", 1_000_000, 65_536),
165];
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::compaction::CompactionPolicy;
171
172 const ZEN: Option<&str> = Some("https://opencode.ai/zen/go/v1");
173
174 #[test]
175 fn recognizes_opencode_go_endpoint() {
176 assert!(is_opencode_go(ZEN));
177 assert!(is_opencode_go(Some("https://opencode.ai/zen/go/v1/")));
178 assert!(is_opencode_go(Some("HTTPS://OPENCODE.AI/ZEN/GO/V1")));
179 assert!(!is_opencode_go(Some("https://api.openai.com/v1")));
180 assert!(!is_opencode_go(Some("https://opencode.ai/zen/v1")));
181 assert!(!is_opencode_go(None));
182 }
183
184 #[test]
185 fn looks_up_configured_model() {
186 let l = model_limit(ZEN, "deepseek-v4.1-flash").expect("known opencode model");
187 assert_eq!(l.context, 1_000_000);
188 assert_eq!(l.output, 384_000);
189 }
190
191 #[test]
192 fn unknown_model_or_provider_is_none() {
193 assert_eq!(model_limit(ZEN, "no-such-model"), None);
194 assert_eq!(model_limit(Some("https://api.openai.com/v1"), "kimi-k3"), None);
196 assert_eq!(model_limit(None, "kimi-k3"), None);
197 }
198
199 #[test]
200 fn step_5_preview_free_is_a_1m_window() {
201 assert_eq!(
205 model_limit(ZEN, "step-5-preview-free"),
206 Some(ModelLimit {
207 context: 1_000_000,
208 output: 65_536,
209 })
210 );
211 }
212
213 #[test]
214 fn catalog_window_beats_the_default_in_compaction() {
215 let p = CompactionPolicy::default();
218 let window = p.context_window(ZEN, "step-5-preview-free");
219 assert_eq!(window, 1_000_000);
220 assert!(
221 !p.should_compact(window, 111_616),
222 "the old 128k trigger must not fire on a 1M model"
223 );
224 assert!(
225 p.should_compact(window, 983_616),
226 "1M window - 16_384 reserved = 983,616"
227 );
228 }
229
230 #[test]
231 fn configured_window_still_wins_only_when_the_catalog_misses() {
232 let p = CompactionPolicy {
233 window: Some(200_000),
234 ..Default::default()
235 };
236 assert_eq!(p.context_window(ZEN, "not-a-real-model"), 200_000);
238 assert_eq!(p.context_window(ZEN, "step-5-preview-free"), 1_000_000);
240 }
241
242 #[test]
243 fn reserve_is_capped_and_trigger_and_is_full_agree() {
244 let big = ModelLimit { context: 1_000_000, output: 384_000 };
246 assert_eq!(big.reserve(), DEFAULT_RESERVE_CAP);
247 assert_eq!(big.trigger(), 1_000_000 - DEFAULT_RESERVE_CAP);
248 assert!(!big.is_full(big.trigger() - 1));
249 assert!(big.is_full(big.trigger()));
250
251 let small = ModelLimit { context: 8_192, output: 4_096 };
253 assert_eq!(small.reserve(), 4_096);
254 assert_eq!(small.trigger(), 4_096);
255 assert!(small.is_full(4_096));
256 }
257
258 #[test]
259 fn model_protocol_matrix() {
260 for id in [
262 "grok-4.6",
263 "grok-4.7",
264 "gpt-5.6-luna",
265 "gpt-6-luna",
266 "muse-spark-1.2-contributor",
267 "muse-spark-1.3-contributor",
268 ] {
269 assert_eq!(model_protocol(ZEN, id), Some(LlmEndpoint::Responses), "{id}");
270 }
271 }
272
273 #[test]
274 fn model_protocol_both_works_and_unknown_are_none() {
275 assert_eq!(model_protocol(ZEN, "deepseek-v4-flash"), None);
277 assert_eq!(model_protocol(ZEN, "no-such-model"), None);
278 }
279
280 #[test]
281 fn model_protocol_needs_a_zen_base() {
282 assert_eq!(
285 model_protocol(Some("https://api.openai.com/v1"), "grok-4.7"),
286 None
287 );
288 assert_eq!(model_protocol(None, "grok-4.7"), None);
289 }
290}