Skip to main content

oxicode_ai/utils/
overflow.rs

1//! Context overflow detection utilities
2//!
3//! Detects when an LLM response indicates the input exceeded the model's context window.
4//! Supports error-based detection (most providers) and silent overflow detection (z.ai, Xiaomi MiMo).
5
6use crate::messages::AssistantMessage;
7
8/// Regex-like patterns to detect context overflow errors from different providers.
9/// Each entry is a substring pattern that indicates an overflow error.
10const OVERFLOW_PATTERNS: &[&str] = &[
11    "prompt is too long",                    // Anthropic token overflow
12    "request_too_large",                     // Anthropic request byte-size overflow (HTTP 413)
13    "input is too long for requested model", // Amazon Bedrock
14    "exceeds the context window",            // OpenAI (Completions & Responses API)
15    "exceeds the maximum number of tokens",  // Google (Gemini)
16    "maximum prompt length",                 // xAI (Grok)
17    "reduce the length of the messages",     // Groq
18    "maximum context length",                // OpenRouter (all backends)
19    "exceeds the limit of",                  // GitHub Copilot
20    "exceeds the available context size",    // llama.cpp server
21    "greater than the context length",       // LM Studio
22    "context window exceeds limit",          // MiniMax
23    "exceeded model token limit",            // Kimi For Coding
24    "too large for model with",              // Mistral
25    "model_context_window_exceeded",         // z.ai non-standard finish_reason
26    "prompt too long",                       // Ollama explicit overflow error
27    "context_length_exceeded",               // Generic (LiteLLM, etc.)
28    "context length exceeded",               // Generic fallback
29    "too many tokens",                       // Generic fallback
30    "token limit exceeded",                  // Generic fallback
31];
32
33/// Patterns that indicate non-overflow errors (e.g., rate limiting, server errors).
34/// Error messages matching any of these are excluded from overflow detection
35/// even if they also match an OVERFLOW_PATTERN.
36const NON_OVERFLOW_PATTERNS: &[&str] = &[
37    "Throttling error:",    // AWS Bedrock non-overflow
38    "Service unavailable:", // AWS Bedrock non-overflow
39    "rate limit",           // Generic rate limiting
40    "too many requests",    // Generic HTTP 429 style
41];
42
43/// Check if an assistant message represents a context overflow error.
44///
45/// This handles three cases:
46/// 1. **Error-based overflow**: Most providers return `stop_reason = "error"` with a
47///    specific error message pattern.
48/// 2. **Silent overflow**: Some providers accept overflow requests and return
49///    successfully. For these, check if `usage.input` exceeds the context window.
50/// 3. **Length-stop overflow**: Some providers (Xiaomi MiMo) truncate input to fill
51///    the context window, leaving no room for output. Returns `stop_reason = "length"`
52///    with `output = 0` and input filling the context window.
53///
54/// # Arguments
55/// * `message` - The assistant message to check
56/// * `context_window` - Optional context window size for detecting silent overflow
57///
58/// # Returns
59/// `true` if the message indicates a context overflow
60pub fn is_context_overflow(message: &AssistantMessage, context_window: Option<usize>) -> bool {
61    // Case 1: Check error message patterns
62    if message.stop_reason == crate::types::StopReason::Error
63        && let Some(ref error_msg) = message.error_message
64    {
65        // Skip messages matching known non-overflow patterns
66        let is_non_overflow = NON_OVERFLOW_PATTERNS
67            .iter()
68            .any(|p: &&str| error_msg.contains(p));
69
70        if !is_non_overflow {
71            let is_overflow = OVERFLOW_PATTERNS
72                .iter()
73                .any(|p: &&str| error_msg.contains(p));
74
75            if is_overflow {
76                return true;
77            }
78        }
79
80        // Special case: Cerebras returns "400 status code (no body)" or "413 status code (no body)"
81        if (error_msg.contains("400") || error_msg.contains("413"))
82            && (error_msg.contains("no body") || error_msg.trim().len() < 50)
83        {
84            return true;
85        }
86    }
87
88    let Some(window) = context_window else {
89        return false;
90    };
91
92    // Case 2: Silent overflow (z.ai style) - successful but usage exceeds context
93    if message.stop_reason == crate::types::StopReason::Stop {
94        let input_tokens = message.usage.input + message.usage.cache_read;
95        if input_tokens > window {
96            return true;
97        }
98    }
99
100    // Case 3: Length-stop overflow (Xiaomi MiMo style)
101    // Server truncates oversized input to fit context window, leaving no room for output.
102    // Returns stopReason "length" with output=0 and input filling the context window.
103    if message.stop_reason == crate::types::StopReason::Length && message.usage.output == 0 {
104        let input_tokens = message.usage.input + message.usage.cache_read;
105        // Use 99% threshold to account for rounding
106        if input_tokens >= (window as f64 * 0.99) as usize {
107            return true;
108        }
109    }
110
111    false
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::types::{Cost, StopReason, Usage};
118
119    fn make_error_message(error: &str) -> AssistantMessage {
120        let mut msg =
121            AssistantMessage::new(crate::types::Api::OpenAiCompletions, "test", "test-model");
122        msg.stop_reason = StopReason::Error;
123        msg.error_message = Some(error.to_string());
124        msg
125    }
126
127    fn make_success_message(input: usize, output: usize) -> AssistantMessage {
128        let mut msg =
129            AssistantMessage::new(crate::types::Api::OpenAiCompletions, "test", "test-model");
130        msg.stop_reason = StopReason::Stop;
131        msg.usage = Usage {
132            input,
133            output,
134            total_tokens: input + output,
135            cache_read: 0,
136            cache_write: 0,
137            cost: Cost::default(),
138        };
139        msg
140    }
141
142    fn make_length_message(input: usize, output: usize) -> AssistantMessage {
143        let mut msg =
144            AssistantMessage::new(crate::types::Api::OpenAiCompletions, "test", "test-model");
145        msg.stop_reason = StopReason::Length;
146        msg.usage = Usage {
147            input,
148            output,
149            total_tokens: input + output,
150            cache_read: 0,
151            cache_write: 0,
152            cost: Cost::default(),
153        };
154        msg
155    }
156
157    #[test]
158    fn test_anthropic_overflow() {
159        let msg = make_error_message("prompt is too long: 213462 tokens > 200000 maximum");
160        assert!(is_context_overflow(&msg, None));
161    }
162
163    #[test]
164    fn test_anthropic_request_too_large() {
165        let msg = make_error_message("request_too_large: Request exceeds maximum size");
166        assert!(is_context_overflow(&msg, None));
167    }
168
169    #[test]
170    fn test_openai_overflow() {
171        let msg = make_error_message("Your input exceeds the context window of this model");
172        assert!(is_context_overflow(&msg, None));
173    }
174
175    #[test]
176    fn test_google_overflow() {
177        let msg = make_error_message(
178            "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
179        );
180        assert!(is_context_overflow(&msg, None));
181    }
182
183    #[test]
184    fn test_xai_overflow() {
185        let msg = make_error_message(
186            "This model's maximum prompt length is 131072 but the request contains 537812 tokens",
187        );
188        assert!(is_context_overflow(&msg, None));
189    }
190
191    #[test]
192    fn test_groq_overflow() {
193        let msg = make_error_message("Please reduce the length of the messages or completion");
194        assert!(is_context_overflow(&msg, None));
195    }
196
197    #[test]
198    fn test_mistral_overflow() {
199        let msg = make_error_message(
200            "Prompt contains X tokens ... too large for model with Y maximum context length",
201        );
202        assert!(is_context_overflow(&msg, None));
203    }
204
205    #[test]
206    fn test_non_overflow_rate_limit() {
207        let msg = make_error_message("rate limit exceeded");
208        assert!(!is_context_overflow(&msg, None));
209    }
210
211    #[test]
212    fn test_non_overflow_throttling() {
213        let msg = make_error_message("Throttling error: Too many tokens, please wait");
214        // "too many tokens" matches an overflow pattern, but "Throttling error:" is a non-overflow pattern
215        assert!(!is_context_overflow(&msg, None));
216    }
217
218    #[test]
219    fn test_silent_overflow() {
220        let msg = make_success_message(150_000, 500);
221        assert!(is_context_overflow(&msg, Some(128_000)));
222    }
223
224    #[test]
225    fn test_no_silent_overflow() {
226        let msg = make_success_message(100_000, 500);
227        assert!(!is_context_overflow(&msg, Some(128_000)));
228    }
229
230    #[test]
231    fn test_length_stop_overflow() {
232        let msg = make_length_message(127_500, 0);
233        assert!(is_context_overflow(&msg, Some(128_000)));
234    }
235
236    #[test]
237    fn test_length_stop_no_overflow() {
238        let msg = make_length_message(100_000, 0);
239        assert!(!is_context_overflow(&msg, Some(128_000)));
240    }
241
242    #[test]
243    fn test_length_stop_with_output() {
244        // Has output, so not a silent overflow
245        let msg = make_length_message(100_000, 500);
246        assert!(!is_context_overflow(&msg, Some(128_000)));
247    }
248
249    #[test]
250    fn test_no_error_no_overflow() {
251        let msg = make_success_message(100, 50);
252        assert!(!is_context_overflow(&msg, None));
253    }
254
255    #[test]
256    fn test_cerebras_overflow() {
257        let msg = make_error_message("400 status code (no body)");
258        assert!(is_context_overflow(&msg, None));
259    }
260
261    #[test]
262    fn test_bedrock_overflow() {
263        let msg = make_error_message("input is too long for requested model");
264        assert!(is_context_overflow(&msg, None));
265    }
266
267    #[test]
268    fn test_llamacpp_overflow() {
269        let msg =
270            make_error_message("the request exceeds the available context size, try increasing it");
271        assert!(is_context_overflow(&msg, None));
272    }
273
274    #[test]
275    fn test_minimax_overflow() {
276        let msg = make_error_message("invalid params, context window exceeds limit");
277        assert!(is_context_overflow(&msg, None));
278    }
279
280    #[test]
281    fn test_kimi_overflow() {
282        let msg = make_error_message(
283            "Your request exceeded model token limit: 128000 (requested: 200000)",
284        );
285        assert!(is_context_overflow(&msg, None));
286    }
287
288    #[test]
289    fn test_generic_context_length_exceeded() {
290        let msg = make_error_message("context_length_exceeded");
291        assert!(is_context_overflow(&msg, None));
292    }
293
294    #[test]
295    fn test_service_unavailable_not_overflow() {
296        let msg = make_error_message("Service unavailable: too many tokens, try again later");
297        assert!(!is_context_overflow(&msg, None));
298    }
299}