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