use reqwest::header::HeaderMap;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RateLimitInfo {
pub requests_limit: Option<u64>,
pub requests_remaining: Option<u64>,
pub requests_reset: Option<String>,
pub tokens_limit: Option<u64>,
pub tokens_remaining: Option<u64>,
pub tokens_reset: Option<String>,
pub input_tokens_limit: Option<u64>,
pub input_tokens_remaining: Option<u64>,
pub input_tokens_reset: Option<String>,
pub output_tokens_limit: Option<u64>,
pub output_tokens_remaining: Option<u64>,
pub output_tokens_reset: Option<String>,
pub retry_after: Option<u64>,
}
impl RateLimitInfo {
pub fn from_headers(headers: &HeaderMap) -> Self {
Self {
requests_limit: parse_u64(headers, "anthropic-ratelimit-requests-limit"),
requests_remaining: parse_u64(headers, "anthropic-ratelimit-requests-remaining"),
requests_reset: parse_string(headers, "anthropic-ratelimit-requests-reset"),
tokens_limit: parse_u64(headers, "anthropic-ratelimit-tokens-limit"),
tokens_remaining: parse_u64(headers, "anthropic-ratelimit-tokens-remaining"),
tokens_reset: parse_string(headers, "anthropic-ratelimit-tokens-reset"),
input_tokens_limit: parse_u64(headers, "anthropic-ratelimit-input-tokens-limit"),
input_tokens_remaining: parse_u64(
headers,
"anthropic-ratelimit-input-tokens-remaining",
),
input_tokens_reset: parse_string(headers, "anthropic-ratelimit-input-tokens-reset"),
output_tokens_limit: parse_u64(headers, "anthropic-ratelimit-output-tokens-limit"),
output_tokens_remaining: parse_u64(
headers,
"anthropic-ratelimit-output-tokens-remaining",
),
output_tokens_reset: parse_string(headers, "anthropic-ratelimit-output-tokens-reset"),
retry_after: parse_u64(headers, "retry-after"),
}
}
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
}
#[derive(Debug, Clone)]
pub struct ApiResponse<T> {
pub body: T,
pub rate_limit: RateLimitInfo,
}
fn parse_u64(headers: &HeaderMap, name: &str) -> Option<u64> {
headers.get(name)?.to_str().ok()?.trim().parse::<u64>().ok()
}
fn parse_string(headers: &HeaderMap, name: &str) -> Option<String> {
Some(headers.get(name)?.to_str().ok()?.trim().to_owned())
}
#[cfg(test)]
mod tests {
use reqwest::header::HeaderMap;
use super::RateLimitInfo;
#[test]
fn parse_all_headers() {
let mut headers = HeaderMap::new();
headers.insert(
"anthropic-ratelimit-requests-limit",
"1000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-requests-remaining",
"999".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-requests-reset",
"2026-04-05T12:00:00Z".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-tokens-limit",
"100000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-tokens-remaining",
"90000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-tokens-reset",
"2026-04-05T12:00:00Z".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-input-tokens-limit",
"80000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-input-tokens-remaining",
"70000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-input-tokens-reset",
"2026-04-05T12:00:00Z".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-output-tokens-limit",
"20000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-output-tokens-remaining",
"18000".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-output-tokens-reset",
"2026-04-05T12:00:00Z".parse().unwrap(),
);
headers.insert("retry-after", "30".parse().unwrap());
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.requests_limit, Some(1000));
assert_eq!(info.requests_remaining, Some(999));
assert_eq!(info.requests_reset.as_deref(), Some("2026-04-05T12:00:00Z"));
assert_eq!(info.tokens_limit, Some(100_000));
assert_eq!(info.tokens_remaining, Some(90_000));
assert_eq!(info.input_tokens_limit, Some(80_000));
assert_eq!(info.input_tokens_remaining, Some(70_000));
assert_eq!(info.output_tokens_limit, Some(20_000));
assert_eq!(info.output_tokens_remaining, Some(18_000));
assert_eq!(info.retry_after, Some(30));
assert!(!info.is_empty());
}
#[test]
fn empty_headers() {
let info = RateLimitInfo::from_headers(&HeaderMap::new());
assert!(info.is_empty());
assert_eq!(info.requests_limit, None);
assert_eq!(info.retry_after, None);
}
#[test]
fn malformed_values_ignored() {
let mut headers = HeaderMap::new();
headers.insert(
"anthropic-ratelimit-requests-limit",
"not-a-number".parse().unwrap(),
);
headers.insert(
"anthropic-ratelimit-tokens-remaining",
"50000".parse().unwrap(),
);
let info = RateLimitInfo::from_headers(&headers);
assert_eq!(info.requests_limit, None);
assert_eq!(info.tokens_remaining, Some(50_000));
}
}