use crate::usage_limits::{UsageLimit, UsageLimitConfig, UsageLimitScope};
use chrono::{DateTime, Utc};
use regex::Regex;
use std::sync::OnceLock;
pub const DEFAULT_PATTERNS: &[&str] = &[
r"Claude AI (?P<scope>weekly|global) usage limit reached\|(?P<epoch>\d+)",
r"Claude AI usage limit reached\|(?P<epoch>\d+)",
];
static COMPILED: OnceLock<Vec<Regex>> = OnceLock::new();
fn compiled_defaults() -> &'static [Regex] {
COMPILED.get_or_init(|| {
DEFAULT_PATTERNS
.iter()
.map(|src| Regex::new(src).expect("Claude usage-limit default pattern is valid regex"))
.collect()
})
}
fn compile_extras(extras: &[String]) -> Vec<Regex> {
extras
.iter()
.filter_map(|src| match Regex::new(src) {
Ok(r) => Some(r),
Err(e) => {
log::warn!("Ignoring invalid claude usage-limit pattern {src:?}: {e}");
None
}
})
.collect()
}
fn scope_from_capture(cap: ®ex::Captures) -> UsageLimitScope {
cap.name("scope")
.map(|m| match m.as_str() {
"weekly" => UsageLimitScope::Weekly,
"global" => UsageLimitScope::Global,
_ => UsageLimitScope::Session,
})
.unwrap_or(UsageLimitScope::Session)
}
fn reset_from_capture(cap: ®ex::Captures) -> Option<DateTime<Utc>> {
let epoch_str = cap.name("epoch")?.as_str();
let epoch: i64 = epoch_str.parse().ok()?;
DateTime::from_timestamp(epoch, 0)
}
pub fn detect_text(line: &str, cfg: &UsageLimitConfig) -> Option<UsageLimit> {
if !cfg.enabled_for("claude") {
return None;
}
for re in compiled_defaults() {
if let Some(cap) = re.captures(line) {
return Some(UsageLimit {
provider: "claude",
scope: scope_from_capture(&cap),
reset_at: reset_from_capture(&cap),
raw: cap
.get(0)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
});
}
}
for re in compile_extras(cfg.extra_patterns_for("claude")) {
if let Some(cap) = re.captures(line) {
return Some(UsageLimit {
provider: "claude",
scope: scope_from_capture(&cap),
reset_at: reset_from_capture(&cap),
raw: cap
.get(0)
.map(|m| m.as_str().to_string())
.unwrap_or_default(),
});
}
}
None
}
pub fn detect_json(value: &serde_json::Value, cfg: &UsageLimitConfig) -> Option<UsageLimit> {
if !cfg.enabled_for("claude") {
return None;
}
let kind = value.get("type").and_then(|v| v.as_str())?;
let subtype = value.get("subtype").and_then(|v| v.as_str());
let error = value.get("error").and_then(|v| v.as_str());
let error_status = value
.get("error_status")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let is_rate_limit = error == Some("rate_limit") || error_status == 429;
let matched = match (kind, subtype) {
("system", Some("api_retry")) if is_rate_limit => true,
("result", Some("error")) if is_rate_limit => true,
_ => false,
};
if !matched {
return None;
}
Some(UsageLimit {
provider: "claude",
scope: UsageLimitScope::Session,
reset_at: None, raw: value.to_string(),
})
}
#[cfg(test)]
#[path = "usage_limits_tests.rs"]
mod tests;