1use std::env;
2use std::fs;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::task::{Context, Poll};
6use std::time::{Duration, Instant};
7
8use futures::Stream;
9use reqwest::header::{HeaderMap, HeaderValue};
10use reqwest::{Client as ReqwestClient, Response, header};
11use serde::Deserialize;
12use tokio::time::sleep;
13
14use crate::AccumulatingStream;
15use crate::backoff::ExponentialBackoff;
16use crate::base_url::validate_base_url;
17use crate::client_logger::ClientLogger;
18use crate::error::{Error, Result};
19use crate::observability::{
20 CLIENT_REQUEST_DURATION, CLIENT_REQUEST_ERRORS, CLIENT_REQUEST_RETRIES, CLIENT_REQUESTS,
21 CLIENT_RETRY_BACKOFF,
22};
23use crate::sse::process_sse;
24use crate::types::{
25 BatchRequest, BatchResultItem, FileObject, Message, MessageBatch, MessageCountTokensParams,
26 MessageCreateParams, MessageStreamEvent, MessageTokensCount, ModelInfo, ModelListParams,
27 ModelListResponse, PaginatedList, SkillObject, ThinkingConfig,
28};
29
30use base64::Engine as _;
31
32fn base64_encode(data: &[u8]) -> String {
34 base64::engine::general_purpose::STANDARD.encode(data)
35}
36
37pub struct LoggingStream<'a> {
43 inner: AccumulatingStream,
44 logger: &'a dyn ClientLogger,
45 receiver: Option<tokio::sync::oneshot::Receiver<Result<Message>>>,
46}
47
48impl<'a> LoggingStream<'a> {
49 fn new(
51 inner: AccumulatingStream,
52 receiver: tokio::sync::oneshot::Receiver<Result<Message>>,
53 logger: &'a dyn ClientLogger,
54 ) -> Self {
55 Self { inner, logger, receiver: Some(receiver) }
56 }
57}
58
59impl Stream for LoggingStream<'_> {
60 type Item = Result<MessageStreamEvent>;
61
62 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
63 let inner = Pin::new(&mut self.inner);
64 match inner.poll_next(cx) {
65 Poll::Ready(Some(Ok(event))) => {
66 self.logger.log_stream_event(&event);
67 Poll::Ready(Some(Ok(event)))
68 }
69 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
70 Poll::Ready(None) => {
71 if let Some(mut receiver) = self.receiver.take()
73 && let Ok(Ok(ref message)) = receiver.try_recv()
74 {
75 self.logger.log_stream_message(message);
76 }
77 Poll::Ready(None)
78 }
79 Poll::Pending => Poll::Pending,
80 }
81 }
82}
83
84const DEFAULT_API_URL: &str = "https://api.anthropic.com";
85const ANTHROPIC_API_VERSION: &str = "2023-06-01";
86const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
87const STRUCTURED_OUTPUTS_BETA: &str = "structured-outputs-2025-11-13";
88
89#[derive(Debug, Clone)]
91pub struct Anthropic {
92 api_key: String,
93 client: ReqwestClient,
94 base_url: String,
95 timeout: Duration,
96 max_retries: usize,
97 throughput_ops_sec: f64,
98 reserve_capacity: f64,
99 cached_headers: Arc<HeaderMap>,
101}
102
103impl Anthropic {
104 fn resolve_api_key(key_value: &str) -> Result<String> {
106 if let Some(stripped) = key_value.strip_prefix("file://") {
107 let path = if stripped.starts_with('/') {
109 stripped.to_string()
111 } else {
112 stripped.to_string()
114 };
115
116 fs::read_to_string(&path).map(|content| content.trim().to_string()).map_err(|e| {
117 Error::validation(
118 format!("Failed to read API key from file '{}': {}", path, e),
119 Some("api_key".to_string()),
120 )
121 })
122 } else {
123 Ok(key_value.to_string())
125 }
126 }
127
128 fn resolve_base_url(env_value: Option<String>) -> Result<String> {
141 match env_value {
142 Some(value) => {
143 validate_base_url(&value)?;
144 Ok(value)
145 }
146 None => Ok(DEFAULT_API_URL.to_string()),
147 }
148 }
149
150 pub fn new(api_key: Option<String>) -> Result<Self> {
167 let api_key = match api_key {
168 Some(key) => Self::resolve_api_key(&key)?,
169 None => {
170 let env_key = env::var("ANTHROPIC_API_KEY").map_err(|_| {
171 Error::authentication(
172 "API key not provided and ANTHROPIC_API_KEY environment variable not set",
173 )
174 })?;
175 Self::resolve_api_key(&env_key)?
176 }
177 };
178
179 let timeout = DEFAULT_TIMEOUT;
180 let client = ReqwestClient::builder()
181 .timeout(timeout)
182 .pool_max_idle_per_host(10) .pool_idle_timeout(Duration::from_secs(90))
184 .tcp_keepalive(Duration::from_secs(60))
185 .build()
186 .map_err(|e| {
187 Error::http_client(format!("Failed to build HTTP client: {e}"), Some(Box::new(e)))
188 })?;
189
190 let cached_headers = Arc::new(Self::build_default_headers(&api_key)?);
192
193 let base_url = Self::resolve_base_url(env::var("ANTHROPIC_BASE_URL").ok())?;
197
198 Ok(Self {
199 api_key,
200 client,
201 base_url,
202 timeout,
203 max_retries: 3,
204 throughput_ops_sec: 1.0 / 60.0,
205 reserve_capacity: 1.0 / 60.0,
206 cached_headers,
207 })
208 }
209
210 pub fn with_base_url(mut self, base_url: String) -> Result<Self> {
241 validate_base_url(&base_url)?;
242 self.base_url = base_url;
243 Ok(self)
244 }
245
246 pub fn base_url(&self) -> &str {
248 &self.base_url
249 }
250
251 pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
255 self.timeout = timeout;
256
257 let client = ReqwestClient::builder()
259 .timeout(timeout)
260 .pool_max_idle_per_host(10)
261 .pool_idle_timeout(Duration::from_secs(90))
262 .tcp_keepalive(Duration::from_secs(60))
263 .build()
264 .map_err(|e| {
265 Error::http_client(
266 "Failed to build HTTP client with new timeout",
267 Some(Box::new(e)),
268 )
269 })?;
270
271 self.client = client;
272 Ok(self)
273 }
274
275 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
279 self.max_retries = max_retries;
280 self
281 }
282
283 pub fn api_key(&self) -> &str {
285 &self.api_key
286 }
287
288 pub fn with_backoff_params(mut self, throughput_ops_sec: f64, reserve_capacity: f64) -> Self {
292 self.throughput_ops_sec = throughput_ops_sec;
293 self.reserve_capacity = reserve_capacity;
294 self
295 }
296
297 pub fn with_base_url_and_timeout(self, base_url: String, timeout: Duration) -> Result<Self> {
301 self.with_base_url(base_url)?.with_timeout(timeout)
302 }
303
304 fn build_default_headers(api_key: &str) -> Result<HeaderMap> {
306 let mut headers = HeaderMap::new();
307 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
308 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
309 headers.insert(
310 "x-api-key",
311 HeaderValue::from_str(api_key).map_err(|e| {
312 Error::validation(
313 format!("Invalid API key format: {e}"),
314 Some("api_key".to_string()),
315 )
316 })?,
317 );
318 headers.insert("anthropic-version", HeaderValue::from_static(ANTHROPIC_API_VERSION));
319 Ok(headers)
320 }
321
322 fn default_headers(&self) -> HeaderMap {
324 (*self.cached_headers).clone()
325 }
326
327 fn build_url(&self, endpoint: &str) -> String {
340 let base = self.base_url.trim_end_matches('/');
341 format!("{}/v1/{}", base, endpoint)
342 }
343
344 async fn retry_with_backoff<F, Fut, T>(&self, operation: F) -> Result<T>
346 where
347 F: Fn() -> Fut,
348 Fut: std::future::Future<Output = Result<T>>,
349 {
350 let backoff = ExponentialBackoff::new(self.throughput_ops_sec, self.reserve_capacity);
351 let mut last_error = None;
352
353 for attempt in 0..=self.max_retries {
354 match operation().await {
355 Ok(result) => return Ok(result),
356 Err(error) => {
357 if !error.is_retryable() {
359 return Err(error);
360 }
361
362 if attempt == self.max_retries {
364 last_error = Some(error);
365 break;
366 }
367
368 let exp_backoff_duration = backoff.next();
370
371 let header_backoff_duration = match &error {
373 Error::RateLimit { retry_after: Some(seconds), .. } => {
374 Some(Duration::from_secs(*seconds))
375 }
376 Error::ServiceUnavailable { retry_after: Some(seconds), .. } => {
377 Some(Duration::from_secs(*seconds))
378 }
379 _ => None,
380 };
381
382 let sleep_duration = match header_backoff_duration {
384 Some(header_duration) => exp_backoff_duration.max(header_duration),
385 None => exp_backoff_duration,
386 };
387
388 CLIENT_REQUEST_RETRIES.click();
389 CLIENT_RETRY_BACKOFF.add(sleep_duration.as_secs_f64());
390 sleep(sleep_duration).await;
391 last_error = Some(error);
392 }
393 }
394 }
395
396 Err(last_error
397 .unwrap_or_else(|| Error::unknown("Failed after retries without capturing error")))
398 }
399
400 async fn process_error_response(response: Response) -> Error {
402 let status = response.status();
403 let status_code = status.as_u16();
404
405 let request_id = response
407 .headers()
408 .get("x-request-id")
409 .and_then(|val| val.to_str().ok())
410 .map(String::from);
411
412 let retry_after = response
413 .headers()
414 .get("retry-after")
415 .and_then(|val| val.to_str().ok())
416 .and_then(|val| val.parse::<u64>().ok());
417
418 #[derive(Deserialize)]
420 struct ErrorResponse {
421 error: Option<ErrorDetail>,
422 }
423
424 #[derive(Deserialize)]
425 struct ErrorDetail {
426 #[serde(rename = "type")]
427 error_type: Option<String>,
428 message: Option<String>,
429 param: Option<String>,
430 }
431
432 let error_body = match response.text().await {
433 Ok(body) => body,
434 Err(e) => {
435 return Error::http_client(
436 format!("Failed to read error response: {e}"),
437 Some(Box::new(e)),
438 );
439 }
440 };
441
442 let parsed_error = serde_json::from_str::<ErrorResponse>(&error_body).ok();
444 let error_type =
445 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.error_type.clone());
446 let error_message = parsed_error
447 .as_ref()
448 .and_then(|e| e.error.as_ref())
449 .and_then(|e| e.message.clone())
450 .unwrap_or_else(|| error_body.clone());
451 let error_param =
452 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.param.clone());
453
454 match status_code {
456 400 => Error::bad_request(error_message, error_param),
457 401 => Error::authentication(error_message),
458 403 => Error::permission(error_message),
459 404 => Error::not_found(error_message, None, None),
460 408 => Error::timeout(error_message, None),
461 429 => Error::rate_limit(error_message, retry_after),
462 500 => Error::internal_server(error_message, request_id),
463 502..=504 => Error::service_unavailable(error_message, retry_after),
464 529 => Error::rate_limit(error_message, retry_after),
465 _ => Error::api(status_code, error_type, error_message, request_id),
466 }
467 }
468
469 fn map_request_error(&self, e: reqwest::Error) -> Error {
471 if e.is_timeout() {
472 Error::timeout(format!("Request timed out: {e}"), Some(self.timeout.as_secs_f64()))
473 } else if e.is_connect() {
474 Error::connection(format!("Connection error: {e}"), Some(Box::new(e)))
475 } else {
476 Error::http_client(format!("Request failed: {e}"), Some(Box::new(e)))
477 }
478 }
479
480 async fn execute_post_request<T: serde::de::DeserializeOwned>(
482 &self,
483 url: &str,
484 body: &impl serde::Serialize,
485 headers: Option<HeaderMap>,
486 ) -> Result<T> {
487 let headers = headers.unwrap_or_else(|| self.default_headers());
488
489 let response = self
490 .client
491 .post(url)
492 .headers(headers)
493 .json(body)
494 .send()
495 .await
496 .map_err(|e| self.map_request_error(e))?;
497
498 if !response.status().is_success() {
499 return Err(Self::process_error_response(response).await);
500 }
501
502 response.json::<T>().await.map_err(|e| {
503 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
504 })
505 }
506
507 async fn execute_get_request<T: serde::de::DeserializeOwned>(
509 &self,
510 url: &str,
511 query_params: Option<&[(String, String)]>,
512 ) -> Result<T> {
513 let mut request = self.client.get(url).headers(self.default_headers());
514
515 if let Some(params) = query_params {
516 for (key, value) in params {
517 request = request.query(&[(key, value)]);
518 }
519 }
520
521 let response = request.send().await.map_err(|e| self.map_request_error(e))?;
522
523 if !response.status().is_success() {
524 return Err(Self::process_error_response(response).await);
525 }
526
527 response.json::<T>().await.map_err(|e| {
528 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
529 })
530 }
531
532 pub async fn send(&self, mut params: MessageCreateParams) -> Result<Message> {
534 let start = Instant::now();
535 CLIENT_REQUESTS.click();
536
537 if let Err(err) = params.validate() {
539 CLIENT_REQUEST_ERRORS.click();
540 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
541 return Err(err);
542 }
543
544 params.stream = false;
546
547 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
549 params.temperature = Some(1.0);
550 }
551
552 let mut headers = self.default_headers();
554
555 if params.requires_structured_outputs_beta() {
557 headers.insert("anthropic-beta", HeaderValue::from_static(STRUCTURED_OUTPUTS_BETA));
558 }
559
560 if params.context_management.is_some() {
562 let existing =
563 headers.get("anthropic-beta").and_then(|v| v.to_str().ok()).unwrap_or("");
564 let new_val = if existing.is_empty() {
565 "context-management-2025-06-27".to_string()
566 } else {
567 format!("{existing},context-management-2025-06-27")
568 };
569 headers.insert(
570 "anthropic-beta",
571 HeaderValue::from_str(&new_val)
572 .unwrap_or_else(|_| HeaderValue::from_static("context-management-2025-06-27")),
573 );
574 }
575
576 if params.speed.is_some() {
578 let existing =
579 headers.get("anthropic-beta").and_then(|v| v.to_str().ok()).unwrap_or("");
580 let new_val = if existing.is_empty() {
581 "fast-mode-2026-02-01".to_string()
582 } else {
583 format!("{existing},fast-mode-2026-02-01")
584 };
585 headers.insert(
586 "anthropic-beta",
587 HeaderValue::from_str(&new_val)
588 .unwrap_or_else(|_| HeaderValue::from_static("fast-mode-2026-02-01")),
589 );
590 }
591
592 let result = self
593 .retry_with_backoff(|| async {
594 let url = self.build_url("messages");
595 self.execute_post_request(&url, ¶ms, Some(headers.clone())).await
596 })
597 .await;
598
599 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
600 if result.is_err() {
601 CLIENT_REQUEST_ERRORS.click();
602 }
603 result
604 }
605
606 pub async fn send_with_logger(
611 &self,
612 params: MessageCreateParams,
613 logger: &dyn ClientLogger,
614 ) -> Result<Message> {
615 let result = self.send(params).await;
616 if let Ok(ref message) = result {
617 logger.log_response(message);
618 }
619 result
620 }
621
622 pub async fn stream(
626 &self,
627 params: &MessageCreateParams,
628 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
629 let start = Instant::now();
630 CLIENT_REQUESTS.click();
631
632 if let Err(err) = params.validate() {
634 CLIENT_REQUEST_ERRORS.click();
635 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
636 return Err(err);
637 }
638
639 let mut params = params.clone();
641 params.stream = true;
642
643 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
645 params.temperature = Some(1.0);
646 }
647
648 let needs_beta = params.requires_structured_outputs_beta();
650
651 let needs_context_mgmt = params.context_management.is_some();
653
654 let needs_fast_mode = params.speed.is_some();
656
657 let response = self
658 .retry_with_backoff(|| async {
659 let url = self.build_url("messages");
660
661 let mut headers = self.default_headers();
662 headers.insert(header::ACCEPT, HeaderValue::from_static("text/event-stream"));
663
664 let mut betas = Vec::new();
666 if needs_beta {
667 betas.push(STRUCTURED_OUTPUTS_BETA);
668 }
669 if needs_context_mgmt {
670 betas.push("context-management-2025-06-27");
671 }
672 if needs_fast_mode {
673 betas.push("fast-mode-2026-02-01");
674 }
675 if !betas.is_empty() {
676 let beta_val = betas.join(",");
677 headers.insert(
678 "anthropic-beta",
679 HeaderValue::from_str(&beta_val)
680 .unwrap_or_else(|_| HeaderValue::from_static(STRUCTURED_OUTPUTS_BETA)),
681 );
682 }
683
684 let response = self
685 .client
686 .post(&url)
687 .headers(headers)
688 .json(¶ms)
689 .send()
690 .await
691 .map_err(|e| self.map_request_error(e))?;
692
693 if !response.status().is_success() {
694 return Err(Self::process_error_response(response).await);
695 }
696
697 Ok(response)
698 })
699 .await;
700
701 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
702 let response = match response {
703 Ok(response) => response,
704 Err(err) => {
705 CLIENT_REQUEST_ERRORS.click();
706 return Err(err);
707 }
708 };
709
710 let stream = response.bytes_stream();
712
713 Ok(process_sse(stream))
715 }
716
717 pub async fn stream_with_logger<'a>(
727 &self,
728 params: &MessageCreateParams,
729 logger: &'a dyn ClientLogger,
730 ) -> Result<LoggingStream<'a>> {
731 let raw_stream = self.stream(params).await?;
732 let (accumulating_stream, receiver) = AccumulatingStream::new(raw_stream);
733 Ok(LoggingStream::new(accumulating_stream, receiver, logger))
734 }
735
736 pub async fn count_tokens(
741 &self,
742 params: MessageCountTokensParams,
743 ) -> Result<MessageTokensCount> {
744 let start = Instant::now();
745 CLIENT_REQUESTS.click();
746 let result = self
747 .retry_with_backoff(|| async {
748 let url = self.build_url("messages/count_tokens");
749 self.execute_post_request(&url, ¶ms, None).await
750 })
751 .await;
752
753 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
754 if result.is_err() {
755 CLIENT_REQUEST_ERRORS.click();
756 }
757 result
758 }
759
760 pub async fn list_models(&self, params: Option<ModelListParams>) -> Result<ModelListResponse> {
765 let start = Instant::now();
766 CLIENT_REQUESTS.click();
767 let result = self
768 .retry_with_backoff(|| async {
769 let url = self.build_url("models");
770
771 let query_params = params.as_ref().map(|p| {
772 let mut params = Vec::new();
773 if let Some(ref after_id) = p.after_id {
774 params.push(("after_id".to_string(), after_id.clone()));
775 }
776 if let Some(ref before_id) = p.before_id {
777 params.push(("before_id".to_string(), before_id.clone()));
778 }
779 if let Some(limit) = p.limit {
780 params.push(("limit".to_string(), limit.to_string()));
781 }
782 params
783 });
784
785 self.execute_get_request(&url, query_params.as_deref()).await
786 })
787 .await;
788
789 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
790 if result.is_err() {
791 CLIENT_REQUEST_ERRORS.click();
792 }
793 result
794 }
795
796 pub async fn get_model(&self, model_id: &str) -> Result<ModelInfo> {
801 let start = Instant::now();
802 CLIENT_REQUESTS.click();
803 let result = self
804 .retry_with_backoff(|| async {
805 let url = self.build_url(&format!("models/{}", model_id));
806 self.execute_get_request(&url, None).await
807 })
808 .await;
809
810 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
811 if result.is_err() {
812 CLIENT_REQUEST_ERRORS.click();
813 }
814 result
815 }
816
817 async fn execute_delete_request(&self, url: &str) -> Result<()> {
821 let response = self
822 .client
823 .delete(url)
824 .headers(self.default_headers())
825 .send()
826 .await
827 .map_err(|e| self.map_request_error(e))?;
828
829 if !response.status().is_success() {
830 return Err(Self::process_error_response(response).await);
831 }
832
833 Ok(())
834 }
835
836 fn pagination_params(
838 before_id: Option<&str>,
839 after_id: Option<&str>,
840 limit: Option<u32>,
841 ) -> Option<Vec<(String, String)>> {
842 let mut params = Vec::new();
843 if let Some(before) = before_id {
844 params.push(("before_id".to_string(), before.to_string()));
845 }
846 if let Some(after) = after_id {
847 params.push(("after_id".to_string(), after.to_string()));
848 }
849 if let Some(lim) = limit {
850 params.push(("limit".to_string(), lim.to_string()));
851 }
852 if params.is_empty() { None } else { Some(params) }
853 }
854
855 pub async fn create_batch(&self, requests: Vec<BatchRequest>) -> Result<MessageBatch> {
859 let start = Instant::now();
860 CLIENT_REQUESTS.click();
861 let body = serde_json::json!({ "requests": requests });
862 let result = self
863 .retry_with_backoff(|| async {
864 let url = self.build_url("messages/batches");
865 self.execute_post_request(&url, &body, None).await
866 })
867 .await;
868 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
869 if result.is_err() {
870 CLIENT_REQUEST_ERRORS.click();
871 }
872 result
873 }
874
875 pub async fn get_batch(&self, batch_id: &str) -> Result<MessageBatch> {
877 let start = Instant::now();
878 CLIENT_REQUESTS.click();
879 let result = self
880 .retry_with_backoff(|| async {
881 let url = self.build_url(&format!("messages/batches/{batch_id}"));
882 self.execute_get_request(&url, None).await
883 })
884 .await;
885 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
886 if result.is_err() {
887 CLIENT_REQUEST_ERRORS.click();
888 }
889 result
890 }
891
892 pub async fn batch_results(&self, batch_id: &str) -> Result<Vec<BatchResultItem>> {
894 let start = Instant::now();
895 CLIENT_REQUESTS.click();
896 let result = self
897 .retry_with_backoff(|| async {
898 let url = self.build_url(&format!("messages/batches/{batch_id}/results"));
899 let response = self
900 .client
901 .get(&url)
902 .headers(self.default_headers())
903 .send()
904 .await
905 .map_err(|e| self.map_request_error(e))?;
906
907 if !response.status().is_success() {
908 return Err(Self::process_error_response(response).await);
909 }
910
911 let text = response.text().await.map_err(|e| {
912 Error::serialization(
913 format!("Failed to read batch results: {e}"),
914 Some(Box::new(e)),
915 )
916 })?;
917
918 let mut items = Vec::new();
919 for line in text.lines() {
920 let trimmed = line.trim();
921 if trimmed.is_empty() {
922 continue;
923 }
924 let item: BatchResultItem = serde_json::from_str(trimmed)?;
925 items.push(item);
926 }
927 Ok(items)
928 })
929 .await;
930 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
931 if result.is_err() {
932 CLIENT_REQUEST_ERRORS.click();
933 }
934 result
935 }
936
937 pub async fn cancel_batch(&self, batch_id: &str) -> Result<MessageBatch> {
939 let start = Instant::now();
940 CLIENT_REQUESTS.click();
941 let result = self
942 .retry_with_backoff(|| async {
943 let url = self.build_url(&format!("messages/batches/{batch_id}/cancel"));
944 self.execute_post_request(&url, &serde_json::json!({}), None).await
945 })
946 .await;
947 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
948 if result.is_err() {
949 CLIENT_REQUEST_ERRORS.click();
950 }
951 result
952 }
953
954 pub async fn delete_batch(&self, batch_id: &str) -> Result<()> {
956 let start = Instant::now();
957 CLIENT_REQUESTS.click();
958 let result = self
959 .retry_with_backoff(|| async {
960 let url = self.build_url(&format!("messages/batches/{batch_id}"));
961 self.execute_delete_request(&url).await
962 })
963 .await;
964 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
965 if result.is_err() {
966 CLIENT_REQUEST_ERRORS.click();
967 }
968 result
969 }
970
971 pub async fn list_batches(
973 &self,
974 before_id: Option<&str>,
975 after_id: Option<&str>,
976 limit: Option<u32>,
977 ) -> Result<PaginatedList<MessageBatch>> {
978 let start = Instant::now();
979 CLIENT_REQUESTS.click();
980 let result = self
981 .retry_with_backoff(|| async {
982 let url = self.build_url("messages/batches");
983 let query = Self::pagination_params(before_id, after_id, limit);
984 self.execute_get_request(&url, query.as_deref()).await
985 })
986 .await;
987 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
988 if result.is_err() {
989 CLIENT_REQUEST_ERRORS.click();
990 }
991 result
992 }
993
994 pub async fn upload_file(
998 &self,
999 data: Vec<u8>,
1000 mime_type: &str,
1001 filename: &str,
1002 purpose: &str,
1003 ) -> Result<FileObject> {
1004 let start = Instant::now();
1005 CLIENT_REQUESTS.click();
1006
1007 let mime_type = mime_type.to_string();
1008 let filename = filename.to_string();
1009 let purpose = purpose.to_string();
1010
1011 let result = self
1012 .retry_with_backoff(|| {
1013 let data = data.clone();
1014 let mime_type = mime_type.clone();
1015 let filename = filename.clone();
1016 let purpose = purpose.clone();
1017 async move {
1018 let url = self.build_url("files");
1019 let part = reqwest::multipart::Part::bytes(data)
1020 .file_name(filename)
1021 .mime_str(&mime_type)
1022 .map_err(|e| {
1023 Error::validation(
1024 format!("Invalid MIME type: {e}"),
1025 Some("mime_type".to_string()),
1026 )
1027 })?;
1028 let form =
1029 reqwest::multipart::Form::new().text("purpose", purpose).part("file", part);
1030
1031 let response = self
1032 .client
1033 .post(&url)
1034 .headers(self.default_headers())
1035 .multipart(form)
1036 .send()
1037 .await
1038 .map_err(|e| self.map_request_error(e))?;
1039
1040 if !response.status().is_success() {
1041 return Err(Self::process_error_response(response).await);
1042 }
1043
1044 response.json::<FileObject>().await.map_err(|e| {
1045 Error::serialization(
1046 format!("Failed to parse file response: {e}"),
1047 Some(Box::new(e)),
1048 )
1049 })
1050 }
1051 })
1052 .await;
1053 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1054 if result.is_err() {
1055 CLIENT_REQUEST_ERRORS.click();
1056 }
1057 result
1058 }
1059
1060 pub async fn get_file(&self, file_id: &str) -> Result<FileObject> {
1062 let start = Instant::now();
1063 CLIENT_REQUESTS.click();
1064 let result = self
1065 .retry_with_backoff(|| async {
1066 let url = self.build_url(&format!("files/{file_id}"));
1067 self.execute_get_request(&url, None).await
1068 })
1069 .await;
1070 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1071 if result.is_err() {
1072 CLIENT_REQUEST_ERRORS.click();
1073 }
1074 result
1075 }
1076
1077 pub async fn delete_file(&self, file_id: &str) -> Result<()> {
1079 let start = Instant::now();
1080 CLIENT_REQUESTS.click();
1081 let result = self
1082 .retry_with_backoff(|| async {
1083 let url = self.build_url(&format!("files/{file_id}"));
1084 self.execute_delete_request(&url).await
1085 })
1086 .await;
1087 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1088 if result.is_err() {
1089 CLIENT_REQUEST_ERRORS.click();
1090 }
1091 result
1092 }
1093
1094 pub async fn list_files(
1096 &self,
1097 before_id: Option<&str>,
1098 after_id: Option<&str>,
1099 limit: Option<u32>,
1100 ) -> Result<PaginatedList<FileObject>> {
1101 let start = Instant::now();
1102 CLIENT_REQUESTS.click();
1103 let result = self
1104 .retry_with_backoff(|| async {
1105 let url = self.build_url("files");
1106 let query = Self::pagination_params(before_id, after_id, limit);
1107 self.execute_get_request(&url, query.as_deref()).await
1108 })
1109 .await;
1110 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1111 if result.is_err() {
1112 CLIENT_REQUEST_ERRORS.click();
1113 }
1114 result
1115 }
1116
1117 pub async fn create_skill(
1121 &self,
1122 name: &str,
1123 description: &str,
1124 content: Vec<u8>,
1125 ) -> Result<SkillObject> {
1126 let start = Instant::now();
1127 CLIENT_REQUESTS.click();
1128 let body = serde_json::json!({
1129 "name": name,
1130 "description": description,
1131 "content": base64_encode(&content),
1132 });
1133 let result = self
1134 .retry_with_backoff(|| async {
1135 let url = self.build_url("skills");
1136 self.execute_post_request(&url, &body, None).await
1137 })
1138 .await;
1139 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1140 if result.is_err() {
1141 CLIENT_REQUEST_ERRORS.click();
1142 }
1143 result
1144 }
1145
1146 pub async fn get_skill(&self, skill_id: &str) -> Result<SkillObject> {
1148 let start = Instant::now();
1149 CLIENT_REQUESTS.click();
1150 let result = self
1151 .retry_with_backoff(|| async {
1152 let url = self.build_url(&format!("skills/{skill_id}"));
1153 self.execute_get_request(&url, None).await
1154 })
1155 .await;
1156 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1157 if result.is_err() {
1158 CLIENT_REQUEST_ERRORS.click();
1159 }
1160 result
1161 }
1162
1163 pub async fn update_skill(&self, skill_id: &str, content: Vec<u8>) -> Result<SkillObject> {
1165 let start = Instant::now();
1166 CLIENT_REQUESTS.click();
1167 let body = serde_json::json!({
1168 "content": base64_encode(&content),
1169 });
1170 let result = self
1171 .retry_with_backoff(|| async {
1172 let url = self.build_url(&format!("skills/{skill_id}"));
1173 self.execute_post_request(&url, &body, None).await
1174 })
1175 .await;
1176 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1177 if result.is_err() {
1178 CLIENT_REQUEST_ERRORS.click();
1179 }
1180 result
1181 }
1182
1183 pub async fn delete_skill(&self, skill_id: &str) -> Result<()> {
1185 let start = Instant::now();
1186 CLIENT_REQUESTS.click();
1187 let result = self
1188 .retry_with_backoff(|| async {
1189 let url = self.build_url(&format!("skills/{skill_id}"));
1190 self.execute_delete_request(&url).await
1191 })
1192 .await;
1193 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1194 if result.is_err() {
1195 CLIENT_REQUEST_ERRORS.click();
1196 }
1197 result
1198 }
1199
1200 pub async fn list_skills(
1202 &self,
1203 before_id: Option<&str>,
1204 after_id: Option<&str>,
1205 limit: Option<u32>,
1206 ) -> Result<PaginatedList<SkillObject>> {
1207 let start = Instant::now();
1208 CLIENT_REQUESTS.click();
1209 let result = self
1210 .retry_with_backoff(|| async {
1211 let url = self.build_url("skills");
1212 let query = Self::pagination_params(before_id, after_id, limit);
1213 self.execute_get_request(&url, query.as_deref()).await
1214 })
1215 .await;
1216 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1217 if result.is_err() {
1218 CLIENT_REQUEST_ERRORS.click();
1219 }
1220 result
1221 }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::*;
1227 use std::sync::Arc;
1228 use std::sync::atomic::{AtomicUsize, Ordering};
1229
1230 #[tokio::test]
1231 async fn retry_logic_with_backoff() {
1232 let client = Anthropic {
1233 api_key: "test".to_string(),
1234 client: ReqwestClient::new(),
1235 base_url: "http://localhost".to_string(),
1236 timeout: Duration::from_secs(1),
1237 max_retries: 2,
1238 throughput_ops_sec: 1.0 / 60.0,
1239 reserve_capacity: 1.0 / 60.0,
1240 cached_headers: Arc::new(HeaderMap::new()),
1241 };
1242
1243 let attempt_counter = Arc::new(AtomicUsize::new(0));
1244 let counter_clone = attempt_counter.clone();
1245
1246 let result = client
1247 .retry_with_backoff(|| {
1248 let counter = counter_clone.clone();
1249 async move {
1250 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1251 match attempt {
1252 0 | 1 => Err(Error::rate_limit("Rate limited", Some(1))),
1253 _ => Ok("success".to_string()),
1254 }
1255 }
1256 })
1257 .await;
1258
1259 assert!(result.is_ok());
1260 assert_eq!(result.unwrap(), "success");
1261 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1262 }
1263
1264 #[tokio::test]
1265 async fn retry_logic_with_non_retryable_error() {
1266 let client = Anthropic {
1267 api_key: "test".to_string(),
1268 client: ReqwestClient::new(),
1269 base_url: "http://localhost".to_string(),
1270 timeout: Duration::from_secs(1),
1271 max_retries: 2,
1272 throughput_ops_sec: 1.0 / 60.0,
1273 reserve_capacity: 1.0 / 60.0,
1274 cached_headers: Arc::new(HeaderMap::new()),
1275 };
1276
1277 let attempt_counter = Arc::new(AtomicUsize::new(0));
1278 let counter_clone = attempt_counter.clone();
1279
1280 let result: Result<String> = client
1281 .retry_with_backoff(|| {
1282 let counter = counter_clone.clone();
1283 async move {
1284 counter.fetch_add(1, Ordering::SeqCst);
1285 Err(Error::authentication("Invalid API key"))
1286 }
1287 })
1288 .await;
1289
1290 assert!(result.is_err());
1291 assert!(result.unwrap_err().is_authentication());
1292 assert_eq!(attempt_counter.load(Ordering::SeqCst), 1);
1294 }
1295
1296 #[tokio::test]
1297 async fn retry_logic_max_retries_exceeded() {
1298 let client = Anthropic {
1299 api_key: "test".to_string(),
1300 client: ReqwestClient::new(),
1301 base_url: "http://localhost".to_string(),
1302 timeout: Duration::from_secs(1),
1303 max_retries: 2,
1304 throughput_ops_sec: 1.0 / 60.0,
1305 reserve_capacity: 1.0 / 60.0,
1306 cached_headers: Arc::new(HeaderMap::new()),
1307 };
1308
1309 let attempt_counter = Arc::new(AtomicUsize::new(0));
1310 let counter_clone = attempt_counter.clone();
1311
1312 let result: Result<String> = client
1313 .retry_with_backoff(|| {
1314 let counter = counter_clone.clone();
1315 async move {
1316 counter.fetch_add(1, Ordering::SeqCst);
1317 Err(Error::rate_limit("Always rate limited", Some(1)))
1318 }
1319 })
1320 .await;
1321
1322 assert!(result.is_err());
1323 assert!(result.unwrap_err().is_rate_limit());
1324 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1326 }
1327
1328 #[tokio::test]
1329 async fn error_529_is_retryable() {
1330 let client = Anthropic {
1332 api_key: "test".to_string(),
1333 client: ReqwestClient::new(),
1334 base_url: "http://localhost".to_string(),
1335 timeout: Duration::from_secs(1),
1336 max_retries: 2,
1337 throughput_ops_sec: 1.0 / 60.0,
1338 reserve_capacity: 1.0 / 60.0,
1339 cached_headers: Arc::new(HeaderMap::new()),
1340 };
1341
1342 let attempt_counter = Arc::new(AtomicUsize::new(0));
1343 let counter_clone = attempt_counter.clone();
1344
1345 let result = client
1346 .retry_with_backoff(|| {
1347 let counter = counter_clone.clone();
1348 async move {
1349 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1350 match attempt {
1351 0 | 1 => {
1352 Err(Error::api(
1354 529,
1355 Some("overloaded_error".to_string()),
1356 "Overloaded".to_string(),
1357 None,
1358 ))
1359 }
1360 _ => Ok("success".to_string()),
1361 }
1362 }
1363 })
1364 .await;
1365
1366 assert!(result.is_ok());
1367 assert_eq!(result.unwrap(), "success");
1368 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1370 }
1371
1372 #[test]
1373 fn error_529_mapped_correctly() {
1374 let error =
1376 Error::api(529, Some("overloaded_error".to_string()), "Overloaded".to_string(), None);
1377 assert!(error.is_retryable());
1378
1379 let rate_limit_error = Error::rate_limit("Overloaded", Some(5));
1381 assert!(rate_limit_error.is_retryable());
1382 }
1383
1384 #[test]
1385 fn resolve_api_key_regular_value() {
1386 let result = Anthropic::resolve_api_key("sk-test-key-123");
1387 assert!(result.is_ok());
1388 assert_eq!(result.unwrap(), "sk-test-key-123");
1389 }
1390
1391 #[test]
1392 fn resolve_api_key_file_url_absolute() {
1393 let test_dir =
1394 std::env::temp_dir().join(format!("adk_anthropic_test_{}", std::process::id()));
1395 std::fs::create_dir_all(&test_dir).unwrap();
1396 let test_file = test_dir.join("test_api_key.txt");
1397 std::fs::write(&test_file, "sk-test-from-file-123\n").unwrap();
1398
1399 let file_url = format!("file://{}", test_file.display());
1400 let result = Anthropic::resolve_api_key(&file_url);
1401
1402 std::fs::remove_dir_all(&test_dir).unwrap();
1403
1404 assert!(result.is_ok());
1405 assert_eq!(result.unwrap(), "sk-test-from-file-123");
1406 }
1407
1408 #[test]
1409 fn resolve_api_key_file_url_relative() {
1410 let test_file = "test_relative_key.txt";
1411 std::fs::write(test_file, "sk-relative-key-456\n").unwrap();
1412
1413 let file_url = format!("file://{}", test_file);
1414 let result = Anthropic::resolve_api_key(&file_url);
1415
1416 std::fs::remove_file(test_file).unwrap();
1417
1418 assert!(result.is_ok());
1419 assert_eq!(result.unwrap(), "sk-relative-key-456");
1420 }
1421
1422 #[test]
1423 fn resolve_api_key_file_url_nonexistent() {
1424 let result = Anthropic::resolve_api_key("file:///nonexistent/path/to/key.txt");
1425 assert!(result.is_err());
1426
1427 let error = result.unwrap_err();
1428 assert!(error.is_validation());
1429 assert!(format!("{}", error).contains("Failed to read API key from file"));
1430 }
1431
1432 #[test]
1433 fn resolve_api_key_file_url_with_whitespace() {
1434 let test_file = "test_whitespace_key.txt";
1435 std::fs::write(test_file, " sk-whitespace-key-789 \n ").unwrap();
1436
1437 let file_url = format!("file://{}", test_file);
1438 let result = Anthropic::resolve_api_key(&file_url);
1439
1440 std::fs::remove_file(test_file).unwrap();
1441
1442 assert!(result.is_ok());
1443 assert_eq!(result.unwrap(), "sk-whitespace-key-789");
1444 }
1445
1446 #[test]
1447 fn client_builder_methods() {
1448 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1449
1450 let configured_client = client
1452 .with_base_url("https://custom.api.com".to_string())
1453 .unwrap()
1454 .with_max_retries(5)
1455 .with_backoff_params(2.0, 1.0);
1456
1457 assert_eq!(configured_client.base_url, "https://custom.api.com");
1458 assert_eq!(configured_client.max_retries, 5);
1459 assert_eq!(configured_client.throughput_ops_sec, 2.0);
1460 assert_eq!(configured_client.reserve_capacity, 1.0);
1461 }
1462
1463 #[test]
1464 fn build_url_default_base() {
1465 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1466 assert_eq!(client.build_url("messages"), "https://api.anthropic.com/v1/messages");
1468 assert_eq!(
1469 client.build_url("messages/count_tokens"),
1470 "https://api.anthropic.com/v1/messages/count_tokens"
1471 );
1472 assert_eq!(client.build_url("models"), "https://api.anthropic.com/v1/models");
1473 }
1474
1475 #[test]
1476 fn build_url_custom_base_without_trailing_slash() {
1477 let client = Anthropic::new(Some("test_key".to_string()))
1478 .unwrap()
1479 .with_base_url("https://api.minimax.io/anthropic".to_string())
1480 .unwrap();
1481 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
1482 }
1483
1484 #[test]
1485 fn build_url_custom_base_with_trailing_slash() {
1486 let client = Anthropic::new(Some("test_key".to_string()))
1487 .unwrap()
1488 .with_base_url("https://api.minimax.io/anthropic/".to_string())
1489 .unwrap();
1490 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
1491 }
1492
1493 #[test]
1494 fn build_url_minimax_china() {
1495 let client = Anthropic::new(Some("test_key".to_string()))
1496 .unwrap()
1497 .with_base_url("https://api.minimaxi.com/anthropic".to_string())
1498 .unwrap();
1499 assert_eq!(client.build_url("messages"), "https://api.minimaxi.com/anthropic/v1/messages");
1500 assert_eq!(
1501 client.build_url(&format!("models/{}", "claude-3-opus")),
1502 "https://api.minimaxi.com/anthropic/v1/models/claude-3-opus"
1503 );
1504 }
1505
1506 #[test]
1507 fn with_base_url_accepts_https() {
1508 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
1509 .unwrap()
1510 .with_base_url("https://gateway.example.com/anthropic".to_string())
1511 .unwrap();
1512 assert_eq!(client.base_url, "https://gateway.example.com/anthropic");
1513 }
1514
1515 #[test]
1516 fn with_base_url_allows_loopback_http_for_local_dev() {
1517 for url in ["http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"] {
1518 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
1519 .unwrap()
1520 .with_base_url(url.to_string())
1521 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
1522 assert_eq!(client.base_url, url);
1523 }
1524 }
1525
1526 #[test]
1527 fn with_base_url_rejects_cleartext_http() {
1528 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1529 .unwrap()
1530 .with_base_url("http://gateway.internal.example.com".to_string())
1531 .expect_err("a non-loopback http base URL must be rejected");
1532
1533 assert!(err.is_validation(), "expected a validation error, got {err}");
1534 let message = err.to_string();
1535 assert!(
1536 message.contains("unencrypted"),
1537 "error should explain the cleartext risk, got: {message}"
1538 );
1539 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
1540 }
1541
1542 #[test]
1543 fn with_base_url_rejects_non_http_schemes_and_garbage() {
1544 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
1545 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1546 .unwrap()
1547 .with_base_url(url.to_string())
1548 .expect_err("non-https, non-loopback base URL must be rejected");
1549 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
1550 }
1551 }
1552
1553 #[test]
1560 fn env_base_url_absent_falls_back_to_default() {
1561 let resolved = Anthropic::resolve_base_url(None).unwrap();
1562 assert_eq!(resolved, DEFAULT_API_URL);
1563 }
1564
1565 #[test]
1566 fn env_base_url_accepts_https() {
1567 let resolved =
1568 Anthropic::resolve_base_url(Some("https://gateway.example.com/anthropic".to_string()))
1569 .unwrap();
1570 assert_eq!(resolved, "https://gateway.example.com/anthropic");
1571 }
1572
1573 #[test]
1574 fn env_base_url_allows_loopback_http_for_local_dev() {
1575 for url in ["http://localhost:11434", "http://127.0.0.1:11434", "http://[::1]:11434"] {
1576 let resolved = Anthropic::resolve_base_url(Some(url.to_string()))
1577 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
1578 assert_eq!(resolved, url);
1579 }
1580 }
1581
1582 #[test]
1583 fn env_base_url_rejects_cleartext_http() {
1584 let err =
1585 Anthropic::resolve_base_url(Some("http://gateway.internal.example.com".to_string()))
1586 .expect_err("a non-loopback http ANTHROPIC_BASE_URL must be rejected");
1587
1588 assert!(err.is_validation(), "expected a validation error, got {err}");
1589 let message = err.to_string();
1590 assert!(
1591 message.contains("unencrypted"),
1592 "error should explain the cleartext risk, got: {message}"
1593 );
1594 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
1595 }
1596
1597 #[test]
1598 fn env_base_url_rejects_non_http_schemes_and_garbage() {
1599 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
1600 let err = Anthropic::resolve_base_url(Some(url.to_string()))
1601 .expect_err("non-https, non-loopback ANTHROPIC_BASE_URL must be rejected");
1602 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
1603 }
1604 }
1605
1606 #[test]
1607 fn with_base_url_and_timeout_rejects_cleartext_http() {
1608 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1609 .unwrap()
1610 .with_base_url_and_timeout(
1611 "http://gateway.internal.example.com".to_string(),
1612 Duration::from_secs(5),
1613 )
1614 .expect_err("a non-loopback http base URL must be rejected");
1615 assert!(err.is_validation(), "expected a validation error, got {err}");
1616 }
1617
1618 #[test]
1619 fn client_timeout_configuration() {
1620 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1621 let timeout = Duration::from_secs(30);
1622
1623 let configured_client = client.with_timeout(timeout).unwrap();
1624 assert_eq!(configured_client.timeout, timeout);
1625 }
1626
1627 #[test]
1628 fn client_cached_headers_performance() {
1629 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1630
1631 let headers1 = client.default_headers();
1633 let headers2 = client.default_headers();
1634
1635 assert_eq!(headers1.len(), headers2.len());
1636 assert!(headers1.contains_key("x-api-key"));
1637 assert!(headers1.contains_key("anthropic-version"));
1638 assert!(headers1.contains_key("content-type"));
1639 }
1640
1641 #[test]
1642 fn request_error_mapping() {
1643 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1644
1645 let _timeout = Duration::from_secs(30);
1648 assert_eq!(client.timeout, DEFAULT_TIMEOUT); }
1650
1651 #[tokio::test]
1652 async fn concurrent_retry_safety() {
1653 use std::sync::atomic::{AtomicUsize, Ordering};
1654 use tokio::spawn;
1655
1656 let client = Anthropic {
1657 api_key: "test".to_string(),
1658 client: ReqwestClient::new(),
1659 base_url: "http://localhost".to_string(),
1660 timeout: Duration::from_secs(1),
1661 max_retries: 1,
1662 throughput_ops_sec: 1.0,
1663 reserve_capacity: 1.0,
1664 cached_headers: Arc::new(HeaderMap::new()),
1665 };
1666
1667 let attempt_counter = Arc::new(AtomicUsize::new(0));
1668 let mut handles = vec![];
1669
1670 for _ in 0..3 {
1672 let client_clone = client.clone();
1673 let counter_clone = attempt_counter.clone();
1674
1675 let handle = spawn(async move {
1676 client_clone
1677 .retry_with_backoff(|| {
1678 let counter = counter_clone.clone();
1679 async move {
1680 counter.fetch_add(1, Ordering::SeqCst);
1681 Ok::<String, Error>("success".to_string())
1682 }
1683 })
1684 .await
1685 });
1686 handles.push(handle);
1687 }
1688
1689 for handle in handles {
1691 let result = handle.await.unwrap();
1692 assert!(result.is_ok());
1693 }
1694
1695 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1697 }
1698}