Skip to main content

claude_usage/
client.rs

1//! HTTP client for the Anthropic usage API.
2//!
3//! This module provides functions to fetch usage data from the Anthropic API.
4//! It handles authentication, headers, and error mapping.
5
6use crate::error::ApiError;
7
8/// Anthropic OAuth usage API endpoint.
9pub const USAGE_API_URL: &str = "https://api.anthropic.com/api/oauth/usage";
10
11/// Required beta header value for OAuth endpoints.
12pub const BETA_HEADER: &str = "oauth-2025-04-20";
13
14/// Fetch raw usage data from the Anthropic API (blocking).
15///
16/// This function makes a synchronous HTTP request to the usage API
17/// and returns the raw JSON response body.
18///
19/// # Arguments
20///
21/// * `token` - OAuth access token for authentication
22///
23/// # Errors
24///
25/// Returns [`ApiError`] if:
26/// - Network request fails
27/// - Server returns 401 (unauthorized)
28/// - Server returns 429 (rate limited)
29/// - Server returns 5xx (server error)
30/// - Server returns unexpected status code
31///
32/// # Security
33///
34/// The token is used only for this request and is not stored.
35#[cfg(feature = "blocking")]
36pub fn fetch_usage_raw(token: &str) -> Result<String, ApiError> {
37    let client = reqwest::blocking::Client::new();
38
39    let response = client
40        .get(USAGE_API_URL)
41        .header("Authorization", format!("Bearer {}", token))
42        .header("anthropic-beta", BETA_HEADER)
43        .send()
44        .map_err(|e| ApiError::Network(e.to_string()))?;
45
46    map_response(response)
47}
48
49/// Map HTTP response to result, handling error status codes.
50#[cfg(feature = "blocking")]
51fn map_response(response: reqwest::blocking::Response) -> Result<String, ApiError> {
52    let status = response.status().as_u16();
53
54    match status {
55        200 => response
56            .text()
57            .map_err(|e| ApiError::Network(e.to_string())),
58        401 => Err(ApiError::Unauthorized),
59        429 => {
60            let retry_after = response
61                .headers()
62                .get("retry-after")
63                .and_then(|v| v.to_str().ok())
64                .map(String::from);
65            Err(ApiError::RateLimited { retry_after })
66        }
67        500..=599 => Err(ApiError::Server(status)),
68        _ => Err(ApiError::Unexpected(status)),
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_api_url_is_correct() {
78        assert_eq!(USAGE_API_URL, "https://api.anthropic.com/api/oauth/usage");
79    }
80
81    #[test]
82    fn test_beta_header_is_correct() {
83        assert_eq!(BETA_HEADER, "oauth-2025-04-20");
84    }
85
86    // Integration test - requires valid token
87    #[test]
88    #[ignore = "requires real API credentials"]
89    #[cfg(feature = "blocking")]
90    fn test_fetch_usage_raw_integration() {
91        // This test requires CLAUDE_CODE_OAUTH_TOKEN env var or real credentials
92        let token = std::env::var("CLAUDE_CODE_OAUTH_TOKEN")
93            .expect("CLAUDE_CODE_OAUTH_TOKEN must be set for integration test");
94
95        let result = fetch_usage_raw(&token);
96        match result {
97            Ok(body) => {
98                assert!(body.contains("five_hour"));
99                assert!(body.contains("seven_day"));
100                println!("API response received successfully");
101            }
102            Err(ApiError::Unauthorized) => {
103                println!("Token is invalid or expired");
104            }
105            Err(e) => {
106                panic!("Unexpected error: {}", e);
107            }
108        }
109    }
110
111    #[test]
112    #[cfg(feature = "blocking")]
113    fn test_fetch_with_invalid_token() {
114        // Test that invalid token returns Unauthorized
115        let result = fetch_usage_raw("invalid-token");
116        assert!(matches!(result, Err(ApiError::Unauthorized)));
117    }
118}