use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct RateLimitInfo {
pub requests_limit: Option<u32>,
pub requests_remaining: Option<u32>,
pub requests_reset: Option<String>,
pub tokens_limit: Option<u32>,
pub tokens_remaining: Option<u32>,
pub tokens_reset: Option<String>,
pub retry_after: Option<Duration>,
}
impl RateLimitInfo {
pub fn from_headers(headers: &HashMap<String, String>) -> Self {
Self {
requests_limit: Self::parse_u32(headers, "anthropic-ratelimit-requests-limit"),
requests_remaining: Self::parse_u32(headers, "anthropic-ratelimit-requests-remaining"),
requests_reset: headers.get("anthropic-ratelimit-requests-reset").cloned(),
tokens_limit: Self::parse_u32(headers, "anthropic-ratelimit-tokens-limit"),
tokens_remaining: Self::parse_u32(headers, "anthropic-ratelimit-tokens-remaining"),
tokens_reset: headers.get("anthropic-ratelimit-tokens-reset").cloned(),
retry_after: Self::parse_retry_after(headers),
}
}
fn parse_u32(headers: &HashMap<String, String>, name: &str) -> Option<u32> {
headers.get(name).and_then(|v| v.trim().parse().ok())
}
fn parse_retry_after(headers: &HashMap<String, String>) -> Option<Duration> {
headers
.get("retry-after")
.and_then(|v| v.trim().parse::<u64>().ok())
.map(Duration::from_secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_all_headers() {
let headers = HashMap::from([
("anthropic-ratelimit-requests-limit".to_string(), "100".to_string()),
("anthropic-ratelimit-requests-remaining".to_string(), "95".to_string()),
("anthropic-ratelimit-requests-reset".to_string(), "2025-02-17T12:00:00Z".to_string()),
("anthropic-ratelimit-tokens-limit".to_string(), "100000".to_string()),
("anthropic-ratelimit-tokens-remaining".to_string(), "90000".to_string()),
("anthropic-ratelimit-tokens-reset".to_string(), "2025-02-17T12:00:00Z".to_string()),
("retry-after".to_string(), "30".to_string()),
]);
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.requests_limit, Some(100));
assert_eq!(info.requests_remaining, Some(95));
assert_eq!(info.requests_reset.as_deref(), Some("2025-02-17T12:00:00Z"));
assert_eq!(info.tokens_limit, Some(100000));
assert_eq!(info.tokens_remaining, Some(90000));
assert_eq!(info.tokens_reset.as_deref(), Some("2025-02-17T12:00:00Z"));
assert_eq!(info.retry_after, Some(Duration::from_secs(30)));
}
#[test]
fn test_parse_empty_headers() {
let headers = HashMap::new();
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info, RateLimitInfo::default());
}
#[test]
fn test_parse_partial_headers() {
let headers = HashMap::from([
("anthropic-ratelimit-requests-remaining".to_string(), "5".to_string()),
("anthropic-ratelimit-tokens-remaining".to_string(), "1000".to_string()),
]);
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.requests_limit, None);
assert_eq!(info.requests_remaining, Some(5));
assert_eq!(info.tokens_limit, None);
assert_eq!(info.tokens_remaining, Some(1000));
assert!(info.retry_after.is_none());
}
#[test]
fn test_parse_invalid_numeric_headers() {
let headers = HashMap::from([
("anthropic-ratelimit-requests-limit".to_string(), "not_a_number".to_string()),
("retry-after".to_string(), "invalid".to_string()),
]);
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.requests_limit, None);
assert!(info.retry_after.is_none());
}
#[test]
fn test_retry_after_with_whitespace() {
let headers = HashMap::from([("retry-after".to_string(), " 60 ".to_string())]);
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.retry_after, Some(Duration::from_secs(60)));
}
}