Skip to main content

wcode_harness/
limits.rs

1//! Per-model context/output limits for recognized providers, so the agent can
2//! size context against the real window instead of guessing.
3//!
4//! The provider's own API does not expose a context window: the OpenCode Go
5//! (zen) `/models` endpoint returns only `id/object/created/owned_by`, and rig
6//! leaves `Model::context_length` empty for every OpenAI-compatible listing
7//! (it is only populated for providers whose listing reports a window, e.g.
8//! Groq/Moonshot/OpenRouter). The authoritative catalog is models.dev, whose
9//! `opencode-go` entry (`api = https://opencode.ai/zen/go/v1`) carries
10//! `limit.context` / `limit.output` per model. [`OPENCODE_GO`] is a snapshot of
11//! that table.
12//!
13//! The *live* measure of how full the context is comes from the provider's own
14//! response usage (`prompt_tokens` → `crate::message::Usage::input_tokens`),
15//! not from here; these limits only say *how much* fits.
16
17/// Context window assumed when neither the model catalog nor config supplies
18/// one: a conservative baseline so an unknown model still compacts in time.
19pub const DEFAULT_CONTEXT_WINDOW: u64 = 128_000;
20
21/// Context window and output ceiling advertised for a model.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub struct ModelLimit {
24    /// Total context window (tokens) the model accepts.
25    pub context: u64,
26    /// Maximum output tokens the provider advertises for one reply.
27    pub output: u64,
28}
29
30/// Cap on how many tokens are held back for the model's reply. The advertised
31/// output ceiling can be huge (OpenCode's `deepseek-v4.1-flash` is 384k), so it
32/// is capped to keep most of the window usable for input.
33pub const DEFAULT_RESERVE_CAP: u64 = 32_768;
34
35impl ModelLimit {
36    /// Tokens reserved for the reply: `min(output, DEFAULT_RESERVE_CAP)`.
37    pub fn reserve(&self) -> u64 {
38        self.output.min(DEFAULT_RESERVE_CAP)
39    }
40
41    /// Input-token count at which the window is considered full: the context
42    /// window minus the reply reserve.
43    pub fn trigger(&self) -> u64 {
44        self.context.saturating_sub(self.reserve())
45    }
46
47    /// True when a provider-reported `input_tokens` has reached the trigger —
48    /// the next request risks running past the window.
49    pub fn is_full(&self, input_tokens: u64) -> bool {
50        input_tokens >= self.trigger()
51    }
52}
53
54/// True for endpoints served by OpenCode's Go (zen) API.
55pub 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
59/// Limits for `model` served from `base_url`, when we recognize the provider.
60/// `None` = unknown provider/model (the caller falls back to a default budget).
61pub 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
78/// OpenCode Go (zen) model table, snapshotted from the models.dev `opencode-go`
79/// catalog: `(model id, context window, max output tokens)`.
80static OPENCODE_GO: &[(&str, u64, u64)] = &[
81    ("deepseek-v4-flash", 1_000_000, 384_000),
82    ("deepseek-v4-flash-vision-exp", 1_000_000, 384_000),
83    ("deepseek-v4-pro", 1_000_000, 384_000),
84    ("deepseek-v4.1-flash", 1_000_000, 384_000),
85    ("glm-5", 202_752, 32_768),
86    ("glm-5.1", 202_752, 32_768),
87    ("glm-5.2", 1_000_000, 131_072),
88    ("glm-5.3", 1_000_000, 131_072),
89    ("glm-5.3-flash", 1_000_000, 131_072),
90    ("gpt-5.6-luna", 1_050_000, 128_000),
91    ("grok-4.5", 500_000, 500_000),
92    ("grok-4.6", 500_000, 500_000),
93    ("hy3", 256_000, 128_000),
94    ("hy4-preview", 1_024_000, 64_000),
95    ("kimi-k2.5", 262_144, 65_536),
96    ("kimi-k2.6", 262_144, 65_536),
97    ("kimi-k2.7-code", 262_144, 262_144),
98    ("kimi-k3", 1_048_576, 131_072),
99    ("longcat-2.0", 1_000_000, 131_072),
100    ("mimo-v2-omni", 262_144, 128_000),
101    ("mimo-v2-pro", 1_048_576, 128_000),
102    ("mimo-v2.5", 1_000_000, 128_000),
103    ("mimo-v2.5-pro", 1_048_576, 128_000),
104    ("minimax-m2.5", 204_800, 65_536),
105    ("minimax-m2.7", 204_800, 131_072),
106    ("minimax-m3", 1_000_000, 131_072),
107    ("muse-spark-1.2-contributor", 1_048_576, 131_072),
108    ("muse-spark-1.3-contributor", 1_048_576, 131_072),
109    ("omen-alpha", 500_000, 128_000),
110    ("ox-alpha-free", 1_000_000, 131_072),
111    ("qwen3.5-plus", 262_144, 65_536),
112    ("qwen3.6-plus", 1_000_000, 65_536),
113    ("qwen3.7-max", 1_000_000, 65_536),
114    ("qwen3.7-plus", 1_000_000, 65_536),
115    ("qwen3.8-flash", 1_000_000, 131_072),
116    ("qwen3.8-max", 1_000_000, 131_072),
117];
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    const ZEN: Option<&str> = Some("https://opencode.ai/zen/go/v1");
124
125    #[test]
126    fn recognizes_opencode_go_endpoint() {
127        assert!(is_opencode_go(ZEN));
128        assert!(is_opencode_go(Some("https://opencode.ai/zen/go/v1/")));
129        assert!(is_opencode_go(Some("HTTPS://OPENCODE.AI/ZEN/GO/V1")));
130        assert!(!is_opencode_go(Some("https://api.openai.com/v1")));
131        assert!(!is_opencode_go(Some("https://opencode.ai/zen/v1")));
132        assert!(!is_opencode_go(None));
133    }
134
135    #[test]
136    fn looks_up_configured_model() {
137        let l = model_limit(ZEN, "deepseek-v4.1-flash").expect("known opencode model");
138        assert_eq!(l.context, 1_000_000);
139        assert_eq!(l.output, 384_000);
140    }
141
142    #[test]
143    fn unknown_model_or_provider_is_none() {
144        assert_eq!(model_limit(ZEN, "no-such-model"), None);
145        // A known OpenCode id behind a non-opencode endpoint is not assumed.
146        assert_eq!(model_limit(Some("https://api.openai.com/v1"), "kimi-k3"), None);
147        assert_eq!(model_limit(None, "kimi-k3"), None);
148    }
149
150    #[test]
151    fn reserve_is_capped_and_trigger_and_is_full_agree() {
152        // Huge advertised output (384k) is capped to the reserve cap.
153        let big = ModelLimit { context: 1_000_000, output: 384_000 };
154        assert_eq!(big.reserve(), DEFAULT_RESERVE_CAP);
155        assert_eq!(big.trigger(), 1_000_000 - DEFAULT_RESERVE_CAP);
156        assert!(!big.is_full(big.trigger() - 1));
157        assert!(big.is_full(big.trigger()));
158
159        // Small advertised output is used verbatim, not the cap.
160        let small = ModelLimit { context: 8_192, output: 4_096 };
161        assert_eq!(small.reserve(), 4_096);
162        assert_eq!(small.trigger(), 4_096);
163        assert!(small.is_full(4_096));
164    }
165}