1use chrono::{DateTime, Datelike, Timelike, Utc};
4use reqwest::{
5 header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT},
6 Client,
7};
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::{
12 cache::FileCache,
13 config::{ApiConfig, CacheConfig},
14 Error, Result,
15};
16
17use super::{
18 AnthropicConfig, AnthropicUsageStats, CostBreakdown, HttpClient, MetricData, RateLimitInfo,
19 StatisticsData, UsageSummary,
20};
21
22#[derive(Debug, Clone)]
24pub struct ApiClient {
25 http: HttpClient,
26 anthropic: Option<AnthropicApiClient>,
27}
28
29impl ApiClient {
30 pub fn new(config: ApiConfig) -> Result<Self> {
32 let http = HttpClient::new(config.clone())?;
33
34 let anthropic = if let Some(anthropic_config) = config.anthropic {
35 Some(AnthropicApiClient::new(anthropic_config)?.with_usage_tracking())
36 } else {
37 None
38 };
39
40 Ok(Self { http, anthropic })
41 }
42
43 pub fn with_cache(config: ApiConfig, cache_config: CacheConfig) -> Result<Self> {
45 let http = HttpClient::new(config.clone())?;
46
47 let cache = Arc::new(FileCache::new(cache_config));
48
49 let anthropic = if let Some(anthropic_config) = config.anthropic {
50 Some(
51 AnthropicApiClient::new(anthropic_config)?
52 .with_cache(cache.clone())
53 .with_usage_tracking(),
54 )
55 } else {
56 None
57 };
58
59 Ok(Self { http, anthropic })
60 }
61
62 pub async fn from_config_with_cache(config: crate::config::Config) -> Result<Self> {
64 let api_config = config.api;
65 let cache_config = config.cache;
66
67 let client = Self::with_cache(api_config, cache_config)?;
68
69 if let Some(anthropic_client) = &client.anthropic {
71 if let Some(cache) = &anthropic_client.cache {
72 cache.init().await?;
73 }
74 }
75
76 Ok(client)
77 }
78
79 pub async fn from_env_with_cache() -> Result<Self> {
81 let config = crate::config::Config::load().await?;
83 Self::from_config_with_cache(config).await
84 }
85
86 pub fn anthropic(&self) -> Option<&AnthropicApiClient> {
88 self.anthropic.as_ref()
89 }
90
91 pub async fn fetch_claude_usage_stats(
93 &self,
94 start_time: DateTime<Utc>,
95 end_time: DateTime<Utc>,
96 ) -> Result<AnthropicUsageStats> {
97 let anthropic_client = self
98 .anthropic
99 .as_ref()
100 .ok_or_else(|| Error::config("Anthropic API not configured"))?;
101
102 anthropic_client
103 .fetch_usage_stats(start_time, end_time)
104 .await
105 }
106
107 pub async fn fetch_rate_limit_info(&self) -> Result<RateLimitInfo> {
109 let anthropic_client = self
110 .anthropic
111 .as_ref()
112 .ok_or_else(|| Error::config("Anthropic API not configured"))?;
113
114 anthropic_client.fetch_rate_limit_info().await
115 }
116
117 pub async fn fetch_billing_info(
119 &self,
120 start_time: DateTime<Utc>,
121 end_time: DateTime<Utc>,
122 ) -> Result<CostBreakdown> {
123 let anthropic_client = self
124 .anthropic
125 .as_ref()
126 .ok_or_else(|| Error::config("Anthropic API not configured"))?;
127
128 anthropic_client
129 .fetch_billing_info(start_time, end_time)
130 .await
131 }
132
133 pub async fn fetch_daily_usage_stats(&self) -> Result<AnthropicUsageStats> {
135 let end_time = Utc::now();
136 let start_time = end_time - chrono::Duration::days(1);
137 self.fetch_claude_usage_stats(start_time, end_time).await
138 }
139
140 pub async fn fetch_weekly_usage_stats(&self) -> Result<AnthropicUsageStats> {
142 let end_time = Utc::now();
143 let start_time = end_time - chrono::Duration::days(7);
144 self.fetch_claude_usage_stats(start_time, end_time).await
145 }
146
147 pub async fn fetch_monthly_usage_stats(&self) -> Result<AnthropicUsageStats> {
149 let end_time = Utc::now();
150 let start_time = end_time - chrono::Duration::days(30);
151 self.fetch_claude_usage_stats(start_time, end_time).await
152 }
153
154 pub async fn fetch_current_month_usage_stats(&self) -> Result<AnthropicUsageStats> {
156 let now = Utc::now();
157 let start_time = now
158 .with_day(1)
159 .unwrap()
160 .with_hour(0)
161 .unwrap()
162 .with_minute(0)
163 .unwrap()
164 .with_second(0)
165 .unwrap();
166 self.fetch_claude_usage_stats(start_time, now).await
167 }
168
169 pub async fn fetch_current_month_billing(&self) -> Result<CostBreakdown> {
171 let now = Utc::now();
172 let start_time = now
173 .with_day(1)
174 .unwrap()
175 .with_hour(0)
176 .unwrap()
177 .with_minute(0)
178 .unwrap()
179 .with_second(0)
180 .unwrap();
181 self.fetch_billing_info(start_time, now).await
182 }
183
184 pub async fn get_usage_summary(&self) -> Result<UsageSummary> {
186 let _anthropic_client = self
187 .anthropic
188 .as_ref()
189 .ok_or_else(|| Error::config("Anthropic API not configured"))?;
190
191 let (daily_stats, weekly_stats, monthly_stats, rate_limit) = tokio::try_join!(
192 self.fetch_daily_usage_stats(),
193 self.fetch_weekly_usage_stats(),
194 self.fetch_monthly_usage_stats(),
195 self.fetch_rate_limit_info()
196 )?;
197
198 Ok(UsageSummary {
199 daily: daily_stats,
200 weekly: weekly_stats,
201 monthly: monthly_stats,
202 rate_limit,
203 timestamp: Utc::now(),
204 })
205 }
206
207 pub async fn submit_statistics(&self, data: &StatisticsData) -> Result<()> {
209 if let Some(base_url) = &self.http.config().base_url {
210 let url = format!("{}/api/v1/statistics", base_url);
211 let response = self.http.client().post(&url).json(data).send().await?;
212
213 if !response.status().is_success() {
214 return Err(Error::api(format!(
215 "Failed to submit statistics: {}",
216 response.status()
217 )));
218 }
219 }
220
221 Ok(())
222 }
223
224 pub async fn get_metrics(&self, query: &str) -> Result<Vec<MetricData>> {
226 if let Some(base_url) = &self.http.config().base_url {
227 let url = format!("{}/api/v1/metrics?q={}", base_url, query);
228 let response = self.http.client().get(&url).send().await?;
229
230 if !response.status().is_success() {
231 return Err(Error::api(format!(
232 "Failed to get metrics: {}",
233 response.status()
234 )));
235 }
236
237 let metrics: Vec<MetricData> = response.json().await?;
238 return Ok(metrics);
239 }
240
241 Ok(vec![])
243 }
244
245 pub async fn health_check(&self) -> Result<bool> {
247 if let Some(base_url) = &self.http.config().base_url {
248 let url = format!("{}/health", base_url);
249 let response = self.http.client().get(&url).send().await?;
250
251 Ok(response.status().is_success())
252 } else {
253 Ok(true) }
255 }
256}
257
258#[derive(Debug, Clone)]
264pub struct AnthropicApiClient {
265 client: Client,
266 config: AnthropicConfig,
267 cache: Option<Arc<FileCache>>,
268 usage_tracker: Option<Arc<super::LocalUsageTracker>>,
269}
270
271impl AnthropicApiClient {
272 pub fn new(config: AnthropicConfig) -> Result<Self> {
274 let mut headers = HeaderMap::new();
275 headers.insert(
276 AUTHORIZATION,
277 HeaderValue::from_str(&format!("Bearer {}", config.api_key))
278 .map_err(|e| Error::config(format!("Invalid API key format: {}", e)))?,
279 );
280 headers.insert(USER_AGENT, HeaderValue::from_static("cstats-client/1.0.0"));
281 headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
282
283 let client = Client::builder()
284 .timeout(Duration::from_secs(config.timeout_seconds))
285 .default_headers(headers)
286 .build()
287 .map_err(Error::Http)?;
288
289 Ok(Self {
290 client,
291 config,
292 cache: None,
293 usage_tracker: None,
294 })
295 }
296
297 pub fn from_env() -> Result<Self> {
299 let api_key = std::env::var("ANTHROPIC_API_KEY")
300 .map_err(|_| Error::config("ANTHROPIC_API_KEY environment variable not set"))?;
301
302 let mut config = AnthropicConfig {
303 api_key,
304 ..Default::default()
305 };
306
307 if let Ok(base_url) = std::env::var("ANTHROPIC_BASE_URL") {
309 config.base_url = base_url;
310 }
311
312 if let Ok(timeout) = std::env::var("ANTHROPIC_TIMEOUT_SECONDS") {
313 if let Ok(timeout_val) = timeout.parse() {
314 config.timeout_seconds = timeout_val;
315 }
316 }
317
318 Self::new(config)
319 }
320
321 pub fn with_cache(mut self, cache: Arc<FileCache>) -> Self {
323 self.cache = Some(cache);
324 self
325 }
326
327 pub fn with_usage_tracking(mut self) -> Self {
329 self.usage_tracker = Some(Arc::new(super::LocalUsageTracker::new()));
330 self
331 }
332
333 pub fn usage_tracker(&self) -> Option<&Arc<super::LocalUsageTracker>> {
335 self.usage_tracker.as_ref()
336 }
337
338 pub async fn fetch_usage_stats(
344 &self,
345 start_time: DateTime<Utc>,
346 end_time: DateTime<Utc>,
347 ) -> Result<AnthropicUsageStats> {
348 if let Some(tracker) = &self.usage_tracker {
350 tracing::info!(
351 "Returning locally tracked usage statistics for period {} to {}",
352 start_time,
353 end_time
354 );
355 return tracker.get_usage_stats(start_time, end_time).await;
356 }
357
358 tracing::warn!(
359 "Anthropic API does not provide public usage statistics endpoints. \
360 Returning mock data for period {} to {}. \
361 Enable local usage tracking for actual statistics.",
362 start_time,
363 end_time
364 );
365
366 Ok(self.create_mock_usage_stats(start_time, end_time))
368 }
369
370 pub async fn fetch_rate_limit_info(&self) -> Result<RateLimitInfo> {
376 if let Some(tracker) = &self.usage_tracker {
378 return tracker.get_rate_limit_info().await;
379 }
380
381 tracing::warn!(
382 "Anthropic API does not provide public rate limit endpoints. \
383 Returning estimated rate limits based on service tier. \
384 Actual rate limits are enforced through response headers."
385 );
386
387 Ok(RateLimitInfo {
390 requests_per_minute: 1000, requests_remaining: 1000, reset_time: Utc::now() + chrono::Duration::seconds(60),
393 tokens_per_minute: Some(50_000), tokens_remaining: Some(50_000), })
396 }
397
398 pub async fn fetch_billing_info(
404 &self,
405 start_time: DateTime<Utc>,
406 end_time: DateTime<Utc>,
407 ) -> Result<CostBreakdown> {
408 tracing::warn!(
409 "Anthropic API does not provide public billing endpoints. \
410 Returning mock data for period {} to {}. \
411 For actual billing information, visit the Anthropic Console at console.anthropic.com.",
412 start_time,
413 end_time
414 );
415
416 Ok(self.create_mock_cost_breakdown(start_time, end_time))
418 }
419
420 pub async fn health_check(&self) -> Result<bool> {
425 let test_payload = serde_json::json!({
427 "model": "claude-3-haiku-20240307",
428 "messages": [{
429 "role": "user",
430 "content": "Hello"
431 }]
432 });
433
434 match self
435 .client
436 .post(format!("{}/v1/messages/count-tokens", self.config.base_url))
437 .json(&test_payload)
438 .send()
439 .await
440 {
441 Ok(response) => {
442 let status = response.status();
445 Ok(status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED)
446 }
447 Err(_) => Ok(false),
448 }
449 }
450
451 fn create_mock_usage_stats(
453 &self,
454 start_time: DateTime<Utc>,
455 end_time: DateTime<Utc>,
456 ) -> AnthropicUsageStats {
457 use super::{ApiCallStats, TokenUsage, UsagePeriod};
458 use std::collections::HashMap;
459
460 AnthropicUsageStats {
461 token_usage: TokenUsage {
462 input_tokens: 0,
463 output_tokens: 0,
464 total_tokens: 0,
465 by_model: HashMap::new(),
466 },
467 api_calls: ApiCallStats {
468 total_calls: 0,
469 successful_calls: 0,
470 failed_calls: 0,
471 avg_response_time_ms: 0.0,
472 by_model: HashMap::new(),
473 hourly_breakdown: vec![],
474 },
475 costs: self.create_mock_cost_breakdown(start_time, end_time),
476 model_usage: vec![],
477 period: UsagePeriod {
478 start: start_time,
479 end: end_time,
480 period_type: "mock".to_string(),
481 },
482 }
483 }
484
485 fn create_mock_cost_breakdown(
487 &self,
488 _start_time: DateTime<Utc>,
489 _end_time: DateTime<Utc>,
490 ) -> CostBreakdown {
491 use super::TokenCostBreakdown;
492 use std::collections::HashMap;
493
494 CostBreakdown {
495 total_cost_usd: 0.0,
496 by_model: HashMap::new(),
497 by_token_type: TokenCostBreakdown {
498 input_cost_usd: 0.0,
499 output_cost_usd: 0.0,
500 },
501 estimated_monthly_cost_usd: 0.0,
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509 use crate::config::{ApiConfig, Config};
510
511 #[tokio::test]
514 #[ignore = "Integration test - requires ANTHROPIC_API_KEY environment variable"]
515 async fn example_anthropic_api_usage() -> Result<()> {
516 let client = ApiClient::from_env_with_cache().await?;
518
519 if let Some(anthropic_client) = client.anthropic() {
521 let is_healthy = anthropic_client.health_check().await?;
522 println!("API health check: {}", is_healthy);
523
524 match client.fetch_rate_limit_info().await {
526 Ok(rate_limit) => {
527 println!(
528 "Rate limit - Remaining: {}, Reset: {:?}",
529 rate_limit.requests_remaining, rate_limit.reset_time
530 );
531 }
532 Err(e) => println!("Failed to fetch rate limit info: {}", e),
533 }
534
535 match client.fetch_daily_usage_stats().await {
537 Ok(daily_stats) => {
538 println!(
539 "Daily usage - Total tokens: {}",
540 daily_stats.token_usage.total_tokens
541 );
542 println!(
543 "Daily usage - API calls: {}",
544 daily_stats.api_calls.total_calls
545 );
546 println!(
547 "Daily usage - Total cost: ${:.4}",
548 daily_stats.costs.total_cost_usd
549 );
550 }
551 Err(e) => println!("Failed to fetch daily usage stats: {}", e),
552 }
553
554 match client.get_usage_summary().await {
556 Ok(summary) => {
557 println!("Usage Summary:");
558 println!(" Daily tokens: {}", summary.daily.token_usage.total_tokens);
559 println!(
560 " Weekly tokens: {}",
561 summary.weekly.token_usage.total_tokens
562 );
563 println!(
564 " Monthly tokens: {}",
565 summary.monthly.token_usage.total_tokens
566 );
567 println!(
568 " Rate limit remaining: {}",
569 summary.rate_limit.requests_remaining
570 );
571 }
572 Err(e) => println!("Failed to fetch usage summary: {}", e),
573 }
574
575 match client.fetch_current_month_billing().await {
577 Ok(billing) => {
578 println!("Current month billing: ${:.4}", billing.total_cost_usd);
579 for (model, cost) in billing.by_model.iter() {
580 println!(" {}: ${:.4}", model, cost.cost_usd);
581 }
582 }
583 Err(e) => println!("Failed to fetch billing info: {}", e),
584 }
585 } else {
586 println!("Anthropic API not configured");
587 }
588
589 Ok(())
590 }
591
592 #[tokio::test]
594 async fn test_config_from_env() -> Result<()> {
595 let config = Config::from_env()?;
597
598 assert_eq!(config.api.timeout_seconds, 30);
600 assert_eq!(config.api.retry_attempts, 3);
601
602 Ok(())
603 }
604
605 #[tokio::test]
607 async fn test_client_without_anthropic() -> Result<()> {
608 let config = ApiConfig::default();
609 let client = ApiClient::new(config)?;
610
611 assert!(client.anthropic().is_none());
613
614 let result = client.fetch_daily_usage_stats().await;
616 assert!(result.is_err());
617
618 Ok(())
619 }
620}