use chrono::Utc;
use regex::Regex;
use crate::types::{CliTool, RateLimitInfo, RateLimitType};
pub struct RateLimitDetector {
patterns: Vec<RateLimitPattern>,
}
struct RateLimitPattern {
regex: Regex,
limit_type: RateLimitType,
}
impl RateLimitDetector {
pub fn new() -> Self {
let mut patterns = Self::build_claude_patterns();
patterns.extend(Self::build_codex_patterns());
Self { patterns }
}
pub fn new_for_tool(tool: CliTool) -> Self {
let patterns = match tool {
CliTool::ClaudeCode => Self::build_claude_patterns(),
CliTool::Codex => Self::build_codex_patterns(),
CliTool::Gemini => Self::build_gemini_patterns(),
};
Self { patterns }
}
fn build_claude_patterns() -> Vec<RateLimitPattern> {
vec![
RateLimitPattern {
regex: Regex::new(r"(?i)rate\s*limit|usage\s*limit|too\s*many\s*requests")
.expect("valid regex"),
limit_type: RateLimitType::Unknown,
},
RateLimitPattern {
regex: Regex::new(r"(?i)session\s*limit|hourly\s*limit|5[- ]?hour")
.expect("valid regex"),
limit_type: RateLimitType::Session,
},
RateLimitPattern {
regex: Regex::new(r"(?i)daily\s*limit|24[- ]?hour").expect("valid regex"),
limit_type: RateLimitType::Daily,
},
RateLimitPattern {
regex: Regex::new(r"(?i)weekly\s*limit|7[- ]?day").expect("valid regex"),
limit_type: RateLimitType::Weekly,
},
]
}
fn build_codex_patterns() -> Vec<RateLimitPattern> {
vec![RateLimitPattern {
regex: Regex::new(r"(?i)rate\s*limit|quota|exceeded").expect("valid regex"),
limit_type: RateLimitType::Unknown,
}]
}
fn build_gemini_patterns() -> Vec<RateLimitPattern> {
vec![RateLimitPattern {
regex: Regex::new(r"(?i)rate\s*limit|quota\s*exceeded|resource\s*exhausted")
.expect("valid regex"),
limit_type: RateLimitType::Unknown,
}]
}
pub fn detect(&self, line: &str) -> Option<RateLimitInfo> {
for pattern in &self.patterns {
if pattern.regex.is_match(line) {
return Some(RateLimitInfo {
limit_type: pattern.limit_type,
resets_at: None,
usage_percent: None,
raw_message: line.to_string(),
detected_at: Utc::now(),
});
}
}
None
}
pub fn detect_all(&self, lines: &[String]) -> Vec<RateLimitInfo> {
lines.iter().filter_map(|line| self.detect(line)).collect()
}
}
impl Default for RateLimitDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_rate_limit() {
let detector = RateLimitDetector::new();
let result = detector.detect("Error: rate limit exceeded. Please wait.");
assert!(result.is_some());
}
#[test]
fn no_false_positive_on_clean_output() {
let detector = RateLimitDetector::new();
let result = detector.detect("Building artifacts for user...");
assert!(result.is_none());
}
#[test]
fn tool_scoped_detector_does_not_mix() {
let claude_detector = RateLimitDetector::new_for_tool(CliTool::ClaudeCode);
assert!(claude_detector.detect("session limit reached").is_some());
}
#[test]
fn detects_session_limit_type() {
let detector = RateLimitDetector::new_for_tool(CliTool::ClaudeCode);
let info = detector.detect("You have hit your session limit for today").unwrap();
assert_eq!(info.limit_type, RateLimitType::Session);
}
#[test]
fn detects_daily_limit_type() {
let detector = RateLimitDetector::new_for_tool(CliTool::ClaudeCode);
let info = detector.detect("daily limit exceeded").unwrap();
assert_eq!(info.limit_type, RateLimitType::Daily);
}
}