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_json_sse, process_sse};
24use crate::types::{
25 BatchRequest, BatchResultItem, FileObject, Message, MessageBatch, MessageCountTokensParams,
26 MessageCreateParams, MessageStreamEvent, MessageTokensCount, ModelInfo, ModelListParams,
27 ModelListResponse, PaginatedList, ServerFallbackMessage, ServerFallbackRequest,
28 ServerFallbackStreamEvent, SkillObject, ThinkingConfig,
29};
30
31use base64::Engine as _;
32
33fn base64_encode(data: &[u8]) -> String {
35 base64::engine::general_purpose::STANDARD.encode(data)
36}
37
38pub struct LoggingStream<'a> {
44 inner: AccumulatingStream,
45 logger: &'a dyn ClientLogger,
46 receiver: Option<tokio::sync::oneshot::Receiver<Result<Message>>>,
47}
48
49impl<'a> LoggingStream<'a> {
50 fn new(
52 inner: AccumulatingStream,
53 receiver: tokio::sync::oneshot::Receiver<Result<Message>>,
54 logger: &'a dyn ClientLogger,
55 ) -> Self {
56 Self { inner, logger, receiver: Some(receiver) }
57 }
58}
59
60impl Stream for LoggingStream<'_> {
61 type Item = Result<MessageStreamEvent>;
62
63 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
64 let inner = Pin::new(&mut self.inner);
65 match inner.poll_next(cx) {
66 Poll::Ready(Some(Ok(event))) => {
67 self.logger.log_stream_event(&event);
68 Poll::Ready(Some(Ok(event)))
69 }
70 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
71 Poll::Ready(None) => {
72 if let Some(mut receiver) = self.receiver.take()
74 && let Ok(Ok(ref message)) = receiver.try_recv()
75 {
76 self.logger.log_stream_message(message);
77 }
78 Poll::Ready(None)
79 }
80 Poll::Pending => Poll::Pending,
81 }
82 }
83}
84
85const DEFAULT_API_URL: &str = "https://api.anthropic.com";
86const ANTHROPIC_API_VERSION: &str = "2023-06-01";
87const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
88const STRUCTURED_OUTPUTS_BETA: &str = "structured-outputs-2025-11-13";
89const SERVER_FALLBACK_BETA: &str = "server-side-fallback-2026-07-01";
90
91#[derive(Debug, Clone)]
93pub struct Anthropic {
94 api_key: String,
95 client: ReqwestClient,
96 base_url: String,
97 timeout: Duration,
98 max_retries: usize,
99 throughput_ops_sec: f64,
100 reserve_capacity: f64,
101 cached_headers: Arc<HeaderMap>,
103}
104
105impl Anthropic {
106 fn resolve_api_key(key_value: &str) -> Result<String> {
108 if let Some(stripped) = key_value.strip_prefix("file://") {
109 let path = if stripped.starts_with('/') {
111 stripped.to_string()
113 } else {
114 stripped.to_string()
116 };
117
118 fs::read_to_string(&path).map(|content| content.trim().to_string()).map_err(|e| {
119 Error::validation(
120 format!("Failed to read API key from file '{}': {}", path, e),
121 Some("api_key".to_string()),
122 )
123 })
124 } else {
125 Ok(key_value.to_string())
127 }
128 }
129
130 fn resolve_base_url(env_value: Option<String>) -> Result<String> {
143 match env_value {
144 Some(value) => {
145 validate_base_url(&value)?;
146 Ok(value)
147 }
148 None => Ok(DEFAULT_API_URL.to_string()),
149 }
150 }
151
152 pub fn new(api_key: Option<String>) -> Result<Self> {
169 let api_key = match api_key {
170 Some(key) => Self::resolve_api_key(&key)?,
171 None => {
172 let env_key = env::var("ANTHROPIC_API_KEY").map_err(|_| {
173 Error::authentication(
174 "API key not provided and ANTHROPIC_API_KEY environment variable not set",
175 )
176 })?;
177 Self::resolve_api_key(&env_key)?
178 }
179 };
180
181 let timeout = DEFAULT_TIMEOUT;
182 let client = ReqwestClient::builder()
183 .timeout(timeout)
184 .pool_max_idle_per_host(10) .pool_idle_timeout(Duration::from_secs(90))
186 .tcp_keepalive(Duration::from_secs(60))
187 .build()
188 .map_err(|e| {
189 Error::http_client(format!("Failed to build HTTP client: {e}"), Some(Box::new(e)))
190 })?;
191
192 let cached_headers = Arc::new(Self::build_default_headers(&api_key)?);
194
195 let base_url = Self::resolve_base_url(env::var("ANTHROPIC_BASE_URL").ok())?;
199
200 Ok(Self {
201 api_key,
202 client,
203 base_url,
204 timeout,
205 max_retries: 3,
206 throughput_ops_sec: 1.0 / 60.0,
207 reserve_capacity: 1.0 / 60.0,
208 cached_headers,
209 })
210 }
211
212 pub fn new_with_auth_token(auth_token: impl Into<String>) -> Result<Self> {
223 Self::new(Some(String::new()))?.with_auth_token(auth_token)
224 }
225
226 pub fn with_auth_token(mut self, auth_token: impl Into<String>) -> Result<Self> {
235 let auth_token = auth_token.into();
236 if auth_token.trim().is_empty() {
237 return Err(Error::validation(
238 "Auth token cannot be empty".to_string(),
239 Some("auth_token".to_string()),
240 ));
241 }
242
243 let mut headers = (*self.cached_headers).clone();
244 headers.remove("x-api-key");
245 let value = HeaderValue::from_str(&format!("Bearer {auth_token}")).map_err(|error| {
246 Error::validation(
247 format!("Invalid auth token format: {error}"),
248 Some("auth_token".to_string()),
249 )
250 })?;
251 headers.insert(header::AUTHORIZATION, value);
252 self.api_key.clear();
253 self.cached_headers = Arc::new(headers);
254 Ok(self)
255 }
256
257 pub fn with_api_version(mut self, api_version: impl Into<String>) -> Result<Self> {
264 let api_version = api_version.into();
265 if api_version.trim().is_empty() {
266 return Err(Error::validation(
267 "API version cannot be empty".to_string(),
268 Some("api_version".to_string()),
269 ));
270 }
271
272 let mut headers = (*self.cached_headers).clone();
273 let value = HeaderValue::from_str(&api_version).map_err(|error| {
274 Error::validation(
275 format!("Invalid API version format: {error}"),
276 Some("api_version".to_string()),
277 )
278 })?;
279 headers.insert("anthropic-version", value);
280 self.cached_headers = Arc::new(headers);
281 Ok(self)
282 }
283
284 pub fn with_base_url(mut self, base_url: String) -> Result<Self> {
315 validate_base_url(&base_url)?;
316 self.base_url = base_url;
317 Ok(self)
318 }
319
320 pub fn base_url(&self) -> &str {
322 &self.base_url
323 }
324
325 pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
329 self.timeout = timeout;
330
331 let client = ReqwestClient::builder()
333 .timeout(timeout)
334 .pool_max_idle_per_host(10)
335 .pool_idle_timeout(Duration::from_secs(90))
336 .tcp_keepalive(Duration::from_secs(60))
337 .build()
338 .map_err(|e| {
339 Error::http_client(
340 "Failed to build HTTP client with new timeout",
341 Some(Box::new(e)),
342 )
343 })?;
344
345 self.client = client;
346 Ok(self)
347 }
348
349 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
353 self.max_retries = max_retries;
354 self
355 }
356
357 pub fn api_key(&self) -> &str {
359 &self.api_key
360 }
361
362 pub fn with_backoff_params(mut self, throughput_ops_sec: f64, reserve_capacity: f64) -> Self {
366 self.throughput_ops_sec = throughput_ops_sec;
367 self.reserve_capacity = reserve_capacity;
368 self
369 }
370
371 pub fn with_base_url_and_timeout(self, base_url: String, timeout: Duration) -> Result<Self> {
375 self.with_base_url(base_url)?.with_timeout(timeout)
376 }
377
378 fn build_default_headers(api_key: &str) -> Result<HeaderMap> {
380 let mut headers = HeaderMap::new();
381 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
382 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
383 headers.insert(
384 "x-api-key",
385 HeaderValue::from_str(api_key).map_err(|e| {
386 Error::validation(
387 format!("Invalid API key format: {e}"),
388 Some("api_key".to_string()),
389 )
390 })?,
391 );
392 headers.insert("anthropic-version", HeaderValue::from_static(ANTHROPIC_API_VERSION));
393 Ok(headers)
394 }
395
396 fn default_headers(&self) -> HeaderMap {
398 (*self.cached_headers).clone()
399 }
400
401 pub fn default_headers_for_request(&self) -> HeaderMap {
408 self.default_headers()
409 }
410
411 fn build_url(&self, endpoint: &str) -> String {
424 let base = self.base_url.trim_end_matches('/');
425 format!("{}/v1/{}", base, endpoint)
426 }
427
428 async fn retry_with_backoff<F, Fut, T>(&self, operation: F) -> Result<T>
430 where
431 F: Fn() -> Fut,
432 Fut: std::future::Future<Output = Result<T>>,
433 {
434 let backoff = ExponentialBackoff::new(self.throughput_ops_sec, self.reserve_capacity);
435 let mut last_error = None;
436
437 for attempt in 0..=self.max_retries {
438 match operation().await {
439 Ok(result) => return Ok(result),
440 Err(error) => {
441 if !error.is_retryable() {
443 return Err(error);
444 }
445
446 if attempt == self.max_retries {
448 last_error = Some(error);
449 break;
450 }
451
452 let exp_backoff_duration = backoff.next();
454
455 let header_backoff_duration = match &error {
457 Error::RateLimit { retry_after: Some(seconds), .. } => {
458 Some(Duration::from_secs(*seconds))
459 }
460 Error::ServiceUnavailable { retry_after: Some(seconds), .. } => {
461 Some(Duration::from_secs(*seconds))
462 }
463 _ => None,
464 };
465
466 let sleep_duration = match header_backoff_duration {
468 Some(header_duration) => exp_backoff_duration.max(header_duration),
469 None => exp_backoff_duration,
470 };
471
472 CLIENT_REQUEST_RETRIES.click();
473 CLIENT_RETRY_BACKOFF.add(sleep_duration.as_secs_f64());
474 sleep(sleep_duration).await;
475 last_error = Some(error);
476 }
477 }
478 }
479
480 Err(last_error
481 .unwrap_or_else(|| Error::unknown("Failed after retries without capturing error")))
482 }
483
484 async fn process_error_response(response: Response) -> Error {
486 let status = response.status();
487 let status_code = status.as_u16();
488
489 let request_id = response
491 .headers()
492 .get("x-request-id")
493 .and_then(|val| val.to_str().ok())
494 .map(String::from);
495
496 let retry_after = response
497 .headers()
498 .get("retry-after")
499 .and_then(|val| val.to_str().ok())
500 .and_then(|val| val.parse::<u64>().ok());
501
502 #[derive(Deserialize)]
504 struct ErrorResponse {
505 error: Option<ErrorDetail>,
506 }
507
508 #[derive(Deserialize)]
509 struct ErrorDetail {
510 #[serde(rename = "type")]
511 error_type: Option<String>,
512 message: Option<String>,
513 param: Option<String>,
514 }
515
516 let error_body = match response.text().await {
517 Ok(body) => body,
518 Err(e) => {
519 return Error::http_client(
520 format!("Failed to read error response: {e}"),
521 Some(Box::new(e)),
522 );
523 }
524 };
525
526 let parsed_error = serde_json::from_str::<ErrorResponse>(&error_body).ok();
528 let error_type =
529 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.error_type.clone());
530 let error_message = parsed_error
531 .as_ref()
532 .and_then(|e| e.error.as_ref())
533 .and_then(|e| e.message.clone())
534 .unwrap_or_else(|| error_body.clone());
535 let error_param =
536 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.param.clone());
537
538 match status_code {
540 400 => Error::bad_request(error_message, error_param),
541 401 => Error::authentication(error_message),
542 403 => Error::permission(error_message),
543 404 => Error::not_found(error_message, None, None),
544 408 => Error::timeout(error_message, None),
545 429 => Error::rate_limit(error_message, retry_after),
546 500 => Error::internal_server(error_message, request_id),
547 502..=504 => Error::service_unavailable(error_message, retry_after),
548 529 => Error::rate_limit(error_message, retry_after),
549 _ => Error::api(status_code, error_type, error_message, request_id),
550 }
551 }
552
553 fn map_request_error(&self, e: reqwest::Error) -> Error {
555 if e.is_timeout() {
556 Error::timeout(format!("Request timed out: {e}"), Some(self.timeout.as_secs_f64()))
557 } else if e.is_connect() {
558 Error::connection(format!("Connection error: {e}"), Some(Box::new(e)))
559 } else {
560 Error::http_client(format!("Request failed: {e}"), Some(Box::new(e)))
561 }
562 }
563
564 async fn execute_post_request<T: serde::de::DeserializeOwned>(
566 &self,
567 url: &str,
568 body: &impl serde::Serialize,
569 headers: Option<HeaderMap>,
570 ) -> Result<T> {
571 let headers = headers.unwrap_or_else(|| self.default_headers());
572
573 let response = self
574 .client
575 .post(url)
576 .headers(headers)
577 .json(body)
578 .send()
579 .await
580 .map_err(|e| self.map_request_error(e))?;
581
582 if !response.status().is_success() {
583 return Err(Self::process_error_response(response).await);
584 }
585
586 response.json::<T>().await.map_err(|e| {
587 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
588 })
589 }
590
591 async fn execute_get_request<T: serde::de::DeserializeOwned>(
593 &self,
594 url: &str,
595 query_params: Option<&[(String, String)]>,
596 ) -> Result<T> {
597 let mut request = self.client.get(url).headers(self.default_headers());
598
599 if let Some(params) = query_params {
600 for (key, value) in params {
601 request = request.query(&[(key, value)]);
602 }
603 }
604
605 let response = request.send().await.map_err(|e| self.map_request_error(e))?;
606
607 if !response.status().is_success() {
608 return Err(Self::process_error_response(response).await);
609 }
610
611 response.json::<T>().await.map_err(|e| {
612 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
613 })
614 }
615
616 fn append_beta_header(headers: &mut HeaderMap, beta: &str) -> Result<()> {
617 if beta.trim().is_empty() {
618 return Err(Error::validation(
619 "Beta identifier cannot be empty".to_string(),
620 Some("anthropic-beta".to_string()),
621 ));
622 }
623 let existing = headers
624 .get("anthropic-beta")
625 .map(HeaderValue::to_str)
626 .transpose()
627 .map_err(|error| {
628 Error::validation(
629 format!("Invalid existing anthropic-beta header: {error}"),
630 Some("anthropic-beta".to_string()),
631 )
632 })?
633 .unwrap_or_default();
634 if existing.split(',').any(|value| value.trim() == beta) {
635 return Ok(());
636 }
637
638 let combined =
639 if existing.is_empty() { beta.to_string() } else { format!("{existing},{beta}") };
640 let value = HeaderValue::from_str(&combined).map_err(|error| {
641 Error::validation(
642 format!("Invalid anthropic-beta header: {error}"),
643 Some("anthropic-beta".to_string()),
644 )
645 })?;
646 headers.insert("anthropic-beta", value);
647 Ok(())
648 }
649
650 fn message_headers(
651 &self,
652 params: &MessageCreateParams,
653 extra_betas: &[&str],
654 streaming: bool,
655 ) -> Result<HeaderMap> {
656 let mut headers = self.default_headers();
657 if streaming {
658 headers.insert(header::ACCEPT, HeaderValue::from_static("text/event-stream"));
659 }
660 if params.requires_structured_outputs_beta() {
661 Self::append_beta_header(&mut headers, STRUCTURED_OUTPUTS_BETA)?;
662 }
663 if params.context_management.is_some() {
664 Self::append_beta_header(&mut headers, "context-management-2025-06-27")?;
665 }
666 if params.speed.is_some() {
667 Self::append_beta_header(&mut headers, "fast-mode-2026-02-01")?;
668 }
669 for beta in extra_betas {
670 Self::append_beta_header(&mut headers, beta)?;
671 }
672 Ok(headers)
673 }
674
675 async fn send_with_resolved_headers(
676 &self,
677 mut params: MessageCreateParams,
678 replacement_headers: Option<HeaderMap>,
679 ) -> Result<Message> {
680 let start = Instant::now();
681 CLIENT_REQUESTS.click();
682
683 if let Err(err) = params.validate() {
685 CLIENT_REQUEST_ERRORS.click();
686 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
687 return Err(err);
688 }
689
690 params.stream = false;
692
693 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
695 params.temperature = Some(1.0);
696 }
697
698 let headers = match replacement_headers {
699 Some(headers) => headers,
700 None => self.message_headers(¶ms, &[], false)?,
701 };
702
703 let result = self
704 .retry_with_backoff(|| async {
705 let url = self.build_url("messages");
706 self.execute_post_request(&url, ¶ms, Some(headers.clone())).await
707 })
708 .await;
709
710 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
711 if result.is_err() {
712 CLIENT_REQUEST_ERRORS.click();
713 }
714 result
715 }
716
717 pub async fn send(&self, params: MessageCreateParams) -> Result<Message> {
719 self.send_with_resolved_headers(params, None).await
720 }
721
722 pub async fn send_with_headers(
735 &self,
736 params: MessageCreateParams,
737 headers: HeaderMap,
738 ) -> Result<Message> {
739 self.send_with_resolved_headers(params, Some(headers)).await
740 }
741
742 pub async fn send_with_betas(
753 &self,
754 params: MessageCreateParams,
755 betas: &[&str],
756 ) -> Result<Message> {
757 let headers = self.message_headers(¶ms, betas, false)?;
758 self.send_with_resolved_headers(params, Some(headers)).await
759 }
760
761 pub async fn send_with_logger(
766 &self,
767 params: MessageCreateParams,
768 logger: &dyn ClientLogger,
769 ) -> Result<Message> {
770 let result = self.send(params).await;
771 if let Ok(ref message) = result {
772 logger.log_response(message);
773 }
774 result
775 }
776
777 async fn stream_with_resolved_headers(
781 &self,
782 params: &MessageCreateParams,
783 replacement_headers: Option<HeaderMap>,
784 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
785 let start = Instant::now();
786 CLIENT_REQUESTS.click();
787
788 if let Err(err) = params.validate() {
790 CLIENT_REQUEST_ERRORS.click();
791 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
792 return Err(err);
793 }
794
795 let mut params = params.clone();
797 params.stream = true;
798
799 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
801 params.temperature = Some(1.0);
802 }
803
804 let headers = match replacement_headers {
805 Some(headers) => headers,
806 None => self.message_headers(¶ms, &[], true)?,
807 };
808
809 let response = self
810 .retry_with_backoff(|| async {
811 let url = self.build_url("messages");
812
813 let response = self
814 .client
815 .post(&url)
816 .headers(headers.clone())
817 .json(¶ms)
818 .send()
819 .await
820 .map_err(|e| self.map_request_error(e))?;
821
822 if !response.status().is_success() {
823 return Err(Self::process_error_response(response).await);
824 }
825
826 Ok(response)
827 })
828 .await;
829
830 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
831 let response = match response {
832 Ok(response) => response,
833 Err(err) => {
834 CLIENT_REQUEST_ERRORS.click();
835 return Err(err);
836 }
837 };
838
839 let stream = response.bytes_stream();
841
842 Ok(process_sse(stream))
844 }
845
846 pub async fn stream(
848 &self,
849 params: &MessageCreateParams,
850 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
851 self.stream_with_resolved_headers(params, None).await
852 }
853
854 pub async fn stream_with_headers(
865 &self,
866 params: &MessageCreateParams,
867 headers: HeaderMap,
868 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
869 self.stream_with_resolved_headers(params, Some(headers)).await
870 }
871
872 pub async fn stream_with_betas(
883 &self,
884 params: &MessageCreateParams,
885 betas: &[&str],
886 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
887 let headers = self.message_headers(params, betas, true)?;
888 self.stream_with_resolved_headers(params, Some(headers)).await
889 }
890
891 pub async fn send_with_server_fallbacks(
901 &self,
902 mut request: ServerFallbackRequest,
903 ) -> Result<ServerFallbackMessage> {
904 let start = Instant::now();
905 CLIENT_REQUESTS.click();
906 if let Err(error) = request.validate() {
907 CLIENT_REQUEST_ERRORS.click();
908 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
909 return Err(error);
910 }
911
912 request.params.stream = false;
913 if matches!(request.params.thinking, Some(ThinkingConfig::Enabled { .. })) {
914 request.params.temperature = Some(1.0);
915 }
916 let headers = self.message_headers(&request.params, &[SERVER_FALLBACK_BETA], false)?;
917 let result = self
918 .retry_with_backoff(|| async {
919 let url = self.build_url("messages");
920 self.execute_post_request(&url, &request, Some(headers.clone())).await
921 })
922 .await;
923
924 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
925 if result.is_err() {
926 CLIENT_REQUEST_ERRORS.click();
927 }
928 result
929 }
930
931 pub async fn stream_with_server_fallbacks(
938 &self,
939 request: &ServerFallbackRequest,
940 ) -> Result<impl Stream<Item = Result<ServerFallbackStreamEvent>> + use<>> {
941 let start = Instant::now();
942 CLIENT_REQUESTS.click();
943 if let Err(error) = request.validate() {
944 CLIENT_REQUEST_ERRORS.click();
945 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
946 return Err(error);
947 }
948
949 let mut request = request.clone();
950 request.params.stream = true;
951 if matches!(request.params.thinking, Some(ThinkingConfig::Enabled { .. })) {
952 request.params.temperature = Some(1.0);
953 }
954 let headers = self.message_headers(&request.params, &[SERVER_FALLBACK_BETA], true)?;
955 let response = self
956 .retry_with_backoff(|| async {
957 let url = self.build_url("messages");
958 let response = self
959 .client
960 .post(&url)
961 .headers(headers.clone())
962 .json(&request)
963 .send()
964 .await
965 .map_err(|error| self.map_request_error(error))?;
966 if !response.status().is_success() {
967 return Err(Self::process_error_response(response).await);
968 }
969 Ok(response)
970 })
971 .await;
972
973 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
974 let response = match response {
975 Ok(response) => response,
976 Err(error) => {
977 CLIENT_REQUEST_ERRORS.click();
978 return Err(error);
979 }
980 };
981 Ok(process_json_sse(response.bytes_stream()))
982 }
983
984 pub async fn stream_with_logger<'a>(
994 &self,
995 params: &MessageCreateParams,
996 logger: &'a dyn ClientLogger,
997 ) -> Result<LoggingStream<'a>> {
998 let raw_stream = self.stream(params).await?;
999 let (accumulating_stream, receiver) = AccumulatingStream::new(raw_stream);
1000 Ok(LoggingStream::new(accumulating_stream, receiver, logger))
1001 }
1002
1003 pub async fn count_tokens(
1008 &self,
1009 params: MessageCountTokensParams,
1010 ) -> Result<MessageTokensCount> {
1011 let start = Instant::now();
1012 CLIENT_REQUESTS.click();
1013 let result = self
1014 .retry_with_backoff(|| async {
1015 let url = self.build_url("messages/count_tokens");
1016 self.execute_post_request(&url, ¶ms, None).await
1017 })
1018 .await;
1019
1020 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1021 if result.is_err() {
1022 CLIENT_REQUEST_ERRORS.click();
1023 }
1024 result
1025 }
1026
1027 pub async fn list_models(&self, params: Option<ModelListParams>) -> Result<ModelListResponse> {
1032 let start = Instant::now();
1033 CLIENT_REQUESTS.click();
1034 let result = self
1035 .retry_with_backoff(|| async {
1036 let url = self.build_url("models");
1037
1038 let query_params = params.as_ref().map(|p| {
1039 let mut params = Vec::new();
1040 if let Some(ref after_id) = p.after_id {
1041 params.push(("after_id".to_string(), after_id.clone()));
1042 }
1043 if let Some(ref before_id) = p.before_id {
1044 params.push(("before_id".to_string(), before_id.clone()));
1045 }
1046 if let Some(limit) = p.limit {
1047 params.push(("limit".to_string(), limit.to_string()));
1048 }
1049 params
1050 });
1051
1052 self.execute_get_request(&url, query_params.as_deref()).await
1053 })
1054 .await;
1055
1056 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1057 if result.is_err() {
1058 CLIENT_REQUEST_ERRORS.click();
1059 }
1060 result
1061 }
1062
1063 pub async fn get_model(&self, model_id: &str) -> Result<ModelInfo> {
1068 let start = Instant::now();
1069 CLIENT_REQUESTS.click();
1070 let result = self
1071 .retry_with_backoff(|| async {
1072 let url = self.build_url(&format!("models/{}", model_id));
1073 self.execute_get_request(&url, None).await
1074 })
1075 .await;
1076
1077 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1078 if result.is_err() {
1079 CLIENT_REQUEST_ERRORS.click();
1080 }
1081 result
1082 }
1083
1084 async fn execute_delete_request(&self, url: &str) -> Result<()> {
1088 let response = self
1089 .client
1090 .delete(url)
1091 .headers(self.default_headers())
1092 .send()
1093 .await
1094 .map_err(|e| self.map_request_error(e))?;
1095
1096 if !response.status().is_success() {
1097 return Err(Self::process_error_response(response).await);
1098 }
1099
1100 Ok(())
1101 }
1102
1103 fn pagination_params(
1105 before_id: Option<&str>,
1106 after_id: Option<&str>,
1107 limit: Option<u32>,
1108 ) -> Option<Vec<(String, String)>> {
1109 let mut params = Vec::new();
1110 if let Some(before) = before_id {
1111 params.push(("before_id".to_string(), before.to_string()));
1112 }
1113 if let Some(after) = after_id {
1114 params.push(("after_id".to_string(), after.to_string()));
1115 }
1116 if let Some(lim) = limit {
1117 params.push(("limit".to_string(), lim.to_string()));
1118 }
1119 if params.is_empty() { None } else { Some(params) }
1120 }
1121
1122 pub async fn create_batch(&self, requests: Vec<BatchRequest>) -> Result<MessageBatch> {
1126 let start = Instant::now();
1127 CLIENT_REQUESTS.click();
1128 let body = serde_json::json!({ "requests": requests });
1129 let result = self
1130 .retry_with_backoff(|| async {
1131 let url = self.build_url("messages/batches");
1132 self.execute_post_request(&url, &body, None).await
1133 })
1134 .await;
1135 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1136 if result.is_err() {
1137 CLIENT_REQUEST_ERRORS.click();
1138 }
1139 result
1140 }
1141
1142 pub async fn get_batch(&self, batch_id: &str) -> Result<MessageBatch> {
1144 let start = Instant::now();
1145 CLIENT_REQUESTS.click();
1146 let result = self
1147 .retry_with_backoff(|| async {
1148 let url = self.build_url(&format!("messages/batches/{batch_id}"));
1149 self.execute_get_request(&url, None).await
1150 })
1151 .await;
1152 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1153 if result.is_err() {
1154 CLIENT_REQUEST_ERRORS.click();
1155 }
1156 result
1157 }
1158
1159 pub async fn batch_results(&self, batch_id: &str) -> Result<Vec<BatchResultItem>> {
1161 let start = Instant::now();
1162 CLIENT_REQUESTS.click();
1163 let result = self
1164 .retry_with_backoff(|| async {
1165 let url = self.build_url(&format!("messages/batches/{batch_id}/results"));
1166 let response = self
1167 .client
1168 .get(&url)
1169 .headers(self.default_headers())
1170 .send()
1171 .await
1172 .map_err(|e| self.map_request_error(e))?;
1173
1174 if !response.status().is_success() {
1175 return Err(Self::process_error_response(response).await);
1176 }
1177
1178 let text = response.text().await.map_err(|e| {
1179 Error::serialization(
1180 format!("Failed to read batch results: {e}"),
1181 Some(Box::new(e)),
1182 )
1183 })?;
1184
1185 let mut items = Vec::new();
1186 for line in text.lines() {
1187 let trimmed = line.trim();
1188 if trimmed.is_empty() {
1189 continue;
1190 }
1191 let item: BatchResultItem = serde_json::from_str(trimmed)?;
1192 items.push(item);
1193 }
1194 Ok(items)
1195 })
1196 .await;
1197 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1198 if result.is_err() {
1199 CLIENT_REQUEST_ERRORS.click();
1200 }
1201 result
1202 }
1203
1204 pub async fn cancel_batch(&self, batch_id: &str) -> Result<MessageBatch> {
1206 let start = Instant::now();
1207 CLIENT_REQUESTS.click();
1208 let result = self
1209 .retry_with_backoff(|| async {
1210 let url = self.build_url(&format!("messages/batches/{batch_id}/cancel"));
1211 self.execute_post_request(&url, &serde_json::json!({}), None).await
1212 })
1213 .await;
1214 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1215 if result.is_err() {
1216 CLIENT_REQUEST_ERRORS.click();
1217 }
1218 result
1219 }
1220
1221 pub async fn delete_batch(&self, batch_id: &str) -> Result<()> {
1223 let start = Instant::now();
1224 CLIENT_REQUESTS.click();
1225 let result = self
1226 .retry_with_backoff(|| async {
1227 let url = self.build_url(&format!("messages/batches/{batch_id}"));
1228 self.execute_delete_request(&url).await
1229 })
1230 .await;
1231 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1232 if result.is_err() {
1233 CLIENT_REQUEST_ERRORS.click();
1234 }
1235 result
1236 }
1237
1238 pub async fn list_batches(
1240 &self,
1241 before_id: Option<&str>,
1242 after_id: Option<&str>,
1243 limit: Option<u32>,
1244 ) -> Result<PaginatedList<MessageBatch>> {
1245 let start = Instant::now();
1246 CLIENT_REQUESTS.click();
1247 let result = self
1248 .retry_with_backoff(|| async {
1249 let url = self.build_url("messages/batches");
1250 let query = Self::pagination_params(before_id, after_id, limit);
1251 self.execute_get_request(&url, query.as_deref()).await
1252 })
1253 .await;
1254 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1255 if result.is_err() {
1256 CLIENT_REQUEST_ERRORS.click();
1257 }
1258 result
1259 }
1260
1261 pub async fn upload_file(
1265 &self,
1266 data: Vec<u8>,
1267 mime_type: &str,
1268 filename: &str,
1269 purpose: &str,
1270 ) -> Result<FileObject> {
1271 let start = Instant::now();
1272 CLIENT_REQUESTS.click();
1273
1274 let mime_type = mime_type.to_string();
1275 let filename = filename.to_string();
1276 let purpose = purpose.to_string();
1277
1278 let result = self
1279 .retry_with_backoff(|| {
1280 let data = data.clone();
1281 let mime_type = mime_type.clone();
1282 let filename = filename.clone();
1283 let purpose = purpose.clone();
1284 async move {
1285 let url = self.build_url("files");
1286 let part = reqwest::multipart::Part::bytes(data)
1287 .file_name(filename)
1288 .mime_str(&mime_type)
1289 .map_err(|e| {
1290 Error::validation(
1291 format!("Invalid MIME type: {e}"),
1292 Some("mime_type".to_string()),
1293 )
1294 })?;
1295 let form =
1296 reqwest::multipart::Form::new().text("purpose", purpose).part("file", part);
1297
1298 let response = self
1299 .client
1300 .post(&url)
1301 .headers(self.default_headers())
1302 .multipart(form)
1303 .send()
1304 .await
1305 .map_err(|e| self.map_request_error(e))?;
1306
1307 if !response.status().is_success() {
1308 return Err(Self::process_error_response(response).await);
1309 }
1310
1311 response.json::<FileObject>().await.map_err(|e| {
1312 Error::serialization(
1313 format!("Failed to parse file response: {e}"),
1314 Some(Box::new(e)),
1315 )
1316 })
1317 }
1318 })
1319 .await;
1320 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1321 if result.is_err() {
1322 CLIENT_REQUEST_ERRORS.click();
1323 }
1324 result
1325 }
1326
1327 pub async fn get_file(&self, file_id: &str) -> Result<FileObject> {
1329 let start = Instant::now();
1330 CLIENT_REQUESTS.click();
1331 let result = self
1332 .retry_with_backoff(|| async {
1333 let url = self.build_url(&format!("files/{file_id}"));
1334 self.execute_get_request(&url, None).await
1335 })
1336 .await;
1337 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1338 if result.is_err() {
1339 CLIENT_REQUEST_ERRORS.click();
1340 }
1341 result
1342 }
1343
1344 pub async fn delete_file(&self, file_id: &str) -> Result<()> {
1346 let start = Instant::now();
1347 CLIENT_REQUESTS.click();
1348 let result = self
1349 .retry_with_backoff(|| async {
1350 let url = self.build_url(&format!("files/{file_id}"));
1351 self.execute_delete_request(&url).await
1352 })
1353 .await;
1354 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1355 if result.is_err() {
1356 CLIENT_REQUEST_ERRORS.click();
1357 }
1358 result
1359 }
1360
1361 pub async fn list_files(
1363 &self,
1364 before_id: Option<&str>,
1365 after_id: Option<&str>,
1366 limit: Option<u32>,
1367 ) -> Result<PaginatedList<FileObject>> {
1368 let start = Instant::now();
1369 CLIENT_REQUESTS.click();
1370 let result = self
1371 .retry_with_backoff(|| async {
1372 let url = self.build_url("files");
1373 let query = Self::pagination_params(before_id, after_id, limit);
1374 self.execute_get_request(&url, query.as_deref()).await
1375 })
1376 .await;
1377 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1378 if result.is_err() {
1379 CLIENT_REQUEST_ERRORS.click();
1380 }
1381 result
1382 }
1383
1384 pub async fn create_skill(
1388 &self,
1389 name: &str,
1390 description: &str,
1391 content: Vec<u8>,
1392 ) -> Result<SkillObject> {
1393 let start = Instant::now();
1394 CLIENT_REQUESTS.click();
1395 let body = serde_json::json!({
1396 "name": name,
1397 "description": description,
1398 "content": base64_encode(&content),
1399 });
1400 let result = self
1401 .retry_with_backoff(|| async {
1402 let url = self.build_url("skills");
1403 self.execute_post_request(&url, &body, None).await
1404 })
1405 .await;
1406 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1407 if result.is_err() {
1408 CLIENT_REQUEST_ERRORS.click();
1409 }
1410 result
1411 }
1412
1413 pub async fn get_skill(&self, skill_id: &str) -> Result<SkillObject> {
1415 let start = Instant::now();
1416 CLIENT_REQUESTS.click();
1417 let result = self
1418 .retry_with_backoff(|| async {
1419 let url = self.build_url(&format!("skills/{skill_id}"));
1420 self.execute_get_request(&url, None).await
1421 })
1422 .await;
1423 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1424 if result.is_err() {
1425 CLIENT_REQUEST_ERRORS.click();
1426 }
1427 result
1428 }
1429
1430 pub async fn update_skill(&self, skill_id: &str, content: Vec<u8>) -> Result<SkillObject> {
1432 let start = Instant::now();
1433 CLIENT_REQUESTS.click();
1434 let body = serde_json::json!({
1435 "content": base64_encode(&content),
1436 });
1437 let result = self
1438 .retry_with_backoff(|| async {
1439 let url = self.build_url(&format!("skills/{skill_id}"));
1440 self.execute_post_request(&url, &body, None).await
1441 })
1442 .await;
1443 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1444 if result.is_err() {
1445 CLIENT_REQUEST_ERRORS.click();
1446 }
1447 result
1448 }
1449
1450 pub async fn delete_skill(&self, skill_id: &str) -> Result<()> {
1452 let start = Instant::now();
1453 CLIENT_REQUESTS.click();
1454 let result = self
1455 .retry_with_backoff(|| async {
1456 let url = self.build_url(&format!("skills/{skill_id}"));
1457 self.execute_delete_request(&url).await
1458 })
1459 .await;
1460 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1461 if result.is_err() {
1462 CLIENT_REQUEST_ERRORS.click();
1463 }
1464 result
1465 }
1466
1467 pub async fn list_skills(
1469 &self,
1470 before_id: Option<&str>,
1471 after_id: Option<&str>,
1472 limit: Option<u32>,
1473 ) -> Result<PaginatedList<SkillObject>> {
1474 let start = Instant::now();
1475 CLIENT_REQUESTS.click();
1476 let result = self
1477 .retry_with_backoff(|| async {
1478 let url = self.build_url("skills");
1479 let query = Self::pagination_params(before_id, after_id, limit);
1480 self.execute_get_request(&url, query.as_deref()).await
1481 })
1482 .await;
1483 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1484 if result.is_err() {
1485 CLIENT_REQUEST_ERRORS.click();
1486 }
1487 result
1488 }
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493 use super::*;
1494 use std::sync::Arc;
1495 use std::sync::atomic::{AtomicUsize, Ordering};
1496
1497 #[tokio::test]
1498 async fn retry_logic_with_backoff() {
1499 let client = Anthropic {
1500 api_key: "test".to_string(),
1501 client: ReqwestClient::new(),
1502 base_url: "http://localhost".to_string(),
1503 timeout: Duration::from_secs(1),
1504 max_retries: 2,
1505 throughput_ops_sec: 1.0 / 60.0,
1506 reserve_capacity: 1.0 / 60.0,
1507 cached_headers: Arc::new(HeaderMap::new()),
1508 };
1509
1510 let attempt_counter = Arc::new(AtomicUsize::new(0));
1511 let counter_clone = attempt_counter.clone();
1512
1513 let result = client
1514 .retry_with_backoff(|| {
1515 let counter = counter_clone.clone();
1516 async move {
1517 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1518 match attempt {
1519 0 | 1 => Err(Error::rate_limit("Rate limited", Some(1))),
1520 _ => Ok("success".to_string()),
1521 }
1522 }
1523 })
1524 .await;
1525
1526 assert!(result.is_ok());
1527 assert_eq!(result.unwrap(), "success");
1528 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1529 }
1530
1531 #[tokio::test]
1532 async fn retry_logic_with_non_retryable_error() {
1533 let client = Anthropic {
1534 api_key: "test".to_string(),
1535 client: ReqwestClient::new(),
1536 base_url: "http://localhost".to_string(),
1537 timeout: Duration::from_secs(1),
1538 max_retries: 2,
1539 throughput_ops_sec: 1.0 / 60.0,
1540 reserve_capacity: 1.0 / 60.0,
1541 cached_headers: Arc::new(HeaderMap::new()),
1542 };
1543
1544 let attempt_counter = Arc::new(AtomicUsize::new(0));
1545 let counter_clone = attempt_counter.clone();
1546
1547 let result: Result<String> = client
1548 .retry_with_backoff(|| {
1549 let counter = counter_clone.clone();
1550 async move {
1551 counter.fetch_add(1, Ordering::SeqCst);
1552 Err(Error::authentication("Invalid API key"))
1553 }
1554 })
1555 .await;
1556
1557 assert!(result.is_err());
1558 assert!(result.unwrap_err().is_authentication());
1559 assert_eq!(attempt_counter.load(Ordering::SeqCst), 1);
1561 }
1562
1563 #[tokio::test]
1564 async fn retry_logic_max_retries_exceeded() {
1565 let client = Anthropic {
1566 api_key: "test".to_string(),
1567 client: ReqwestClient::new(),
1568 base_url: "http://localhost".to_string(),
1569 timeout: Duration::from_secs(1),
1570 max_retries: 2,
1571 throughput_ops_sec: 1.0 / 60.0,
1572 reserve_capacity: 1.0 / 60.0,
1573 cached_headers: Arc::new(HeaderMap::new()),
1574 };
1575
1576 let attempt_counter = Arc::new(AtomicUsize::new(0));
1577 let counter_clone = attempt_counter.clone();
1578
1579 let result: Result<String> = client
1580 .retry_with_backoff(|| {
1581 let counter = counter_clone.clone();
1582 async move {
1583 counter.fetch_add(1, Ordering::SeqCst);
1584 Err(Error::rate_limit("Always rate limited", Some(1)))
1585 }
1586 })
1587 .await;
1588
1589 assert!(result.is_err());
1590 assert!(result.unwrap_err().is_rate_limit());
1591 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1593 }
1594
1595 #[tokio::test]
1596 async fn error_529_is_retryable() {
1597 let client = Anthropic {
1599 api_key: "test".to_string(),
1600 client: ReqwestClient::new(),
1601 base_url: "http://localhost".to_string(),
1602 timeout: Duration::from_secs(1),
1603 max_retries: 2,
1604 throughput_ops_sec: 1.0 / 60.0,
1605 reserve_capacity: 1.0 / 60.0,
1606 cached_headers: Arc::new(HeaderMap::new()),
1607 };
1608
1609 let attempt_counter = Arc::new(AtomicUsize::new(0));
1610 let counter_clone = attempt_counter.clone();
1611
1612 let result = client
1613 .retry_with_backoff(|| {
1614 let counter = counter_clone.clone();
1615 async move {
1616 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1617 match attempt {
1618 0 | 1 => {
1619 Err(Error::api(
1621 529,
1622 Some("overloaded_error".to_string()),
1623 "Overloaded".to_string(),
1624 None,
1625 ))
1626 }
1627 _ => Ok("success".to_string()),
1628 }
1629 }
1630 })
1631 .await;
1632
1633 assert!(result.is_ok());
1634 assert_eq!(result.unwrap(), "success");
1635 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1637 }
1638
1639 #[test]
1640 fn error_529_mapped_correctly() {
1641 let error =
1643 Error::api(529, Some("overloaded_error".to_string()), "Overloaded".to_string(), None);
1644 assert!(error.is_retryable());
1645
1646 let rate_limit_error = Error::rate_limit("Overloaded", Some(5));
1648 assert!(rate_limit_error.is_retryable());
1649 }
1650
1651 #[test]
1652 fn resolve_api_key_regular_value() {
1653 let result = Anthropic::resolve_api_key("sk-test-key-123");
1654 assert!(result.is_ok());
1655 assert_eq!(result.unwrap(), "sk-test-key-123");
1656 }
1657
1658 #[test]
1659 fn resolve_api_key_file_url_absolute() {
1660 let test_dir =
1661 std::env::temp_dir().join(format!("adk_anthropic_test_{}", std::process::id()));
1662 std::fs::create_dir_all(&test_dir).unwrap();
1663 let test_file = test_dir.join("test_api_key.txt");
1664 std::fs::write(&test_file, "sk-test-from-file-123\n").unwrap();
1665
1666 let file_url = format!("file://{}", test_file.display());
1667 let result = Anthropic::resolve_api_key(&file_url);
1668
1669 std::fs::remove_dir_all(&test_dir).unwrap();
1670
1671 assert!(result.is_ok());
1672 assert_eq!(result.unwrap(), "sk-test-from-file-123");
1673 }
1674
1675 #[test]
1676 fn resolve_api_key_file_url_relative() {
1677 let test_file = "test_relative_key.txt";
1678 std::fs::write(test_file, "sk-relative-key-456\n").unwrap();
1679
1680 let file_url = format!("file://{}", test_file);
1681 let result = Anthropic::resolve_api_key(&file_url);
1682
1683 std::fs::remove_file(test_file).unwrap();
1684
1685 assert!(result.is_ok());
1686 assert_eq!(result.unwrap(), "sk-relative-key-456");
1687 }
1688
1689 #[test]
1690 fn resolve_api_key_file_url_nonexistent() {
1691 let result = Anthropic::resolve_api_key("file:///nonexistent/path/to/key.txt");
1692 assert!(result.is_err());
1693
1694 let error = result.unwrap_err();
1695 assert!(error.is_validation());
1696 assert!(format!("{}", error).contains("Failed to read API key from file"));
1697 }
1698
1699 #[test]
1700 fn resolve_api_key_file_url_with_whitespace() {
1701 let test_file = "test_whitespace_key.txt";
1702 std::fs::write(test_file, " sk-whitespace-key-789 \n ").unwrap();
1703
1704 let file_url = format!("file://{}", test_file);
1705 let result = Anthropic::resolve_api_key(&file_url);
1706
1707 std::fs::remove_file(test_file).unwrap();
1708
1709 assert!(result.is_ok());
1710 assert_eq!(result.unwrap(), "sk-whitespace-key-789");
1711 }
1712
1713 #[test]
1714 fn client_builder_methods() {
1715 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1716
1717 let configured_client = client
1719 .with_base_url("https://custom.api.com".to_string())
1720 .unwrap()
1721 .with_max_retries(5)
1722 .with_backoff_params(2.0, 1.0);
1723
1724 assert_eq!(configured_client.base_url, "https://custom.api.com");
1725 assert_eq!(configured_client.max_retries, 5);
1726 assert_eq!(configured_client.throughput_ops_sec, 2.0);
1727 assert_eq!(configured_client.reserve_capacity, 1.0);
1728 }
1729
1730 #[test]
1731 fn build_url_default_base() {
1732 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1733 assert_eq!(client.build_url("messages"), "https://api.anthropic.com/v1/messages");
1735 assert_eq!(
1736 client.build_url("messages/count_tokens"),
1737 "https://api.anthropic.com/v1/messages/count_tokens"
1738 );
1739 assert_eq!(client.build_url("models"), "https://api.anthropic.com/v1/models");
1740 }
1741
1742 #[test]
1743 fn build_url_custom_base_without_trailing_slash() {
1744 let client = Anthropic::new(Some("test_key".to_string()))
1745 .unwrap()
1746 .with_base_url("https://api.minimax.io/anthropic".to_string())
1747 .unwrap();
1748 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
1749 }
1750
1751 #[test]
1752 fn build_url_custom_base_with_trailing_slash() {
1753 let client = Anthropic::new(Some("test_key".to_string()))
1754 .unwrap()
1755 .with_base_url("https://api.minimax.io/anthropic/".to_string())
1756 .unwrap();
1757 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
1758 }
1759
1760 #[test]
1761 fn build_url_minimax_china() {
1762 let client = Anthropic::new(Some("test_key".to_string()))
1763 .unwrap()
1764 .with_base_url("https://api.minimaxi.com/anthropic".to_string())
1765 .unwrap();
1766 assert_eq!(client.build_url("messages"), "https://api.minimaxi.com/anthropic/v1/messages");
1767 assert_eq!(
1768 client.build_url(&format!("models/{}", "claude-3-opus")),
1769 "https://api.minimaxi.com/anthropic/v1/models/claude-3-opus"
1770 );
1771 }
1772
1773 #[test]
1774 fn with_base_url_accepts_https() {
1775 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
1776 .unwrap()
1777 .with_base_url("https://gateway.example.com/anthropic".to_string())
1778 .unwrap();
1779 assert_eq!(client.base_url, "https://gateway.example.com/anthropic");
1780 }
1781
1782 #[test]
1783 fn with_base_url_allows_loopback_http_for_local_dev() {
1784 for url in ["http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"] {
1785 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
1786 .unwrap()
1787 .with_base_url(url.to_string())
1788 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
1789 assert_eq!(client.base_url, url);
1790 }
1791 }
1792
1793 #[test]
1794 fn with_base_url_rejects_cleartext_http() {
1795 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1796 .unwrap()
1797 .with_base_url("http://gateway.internal.example.com".to_string())
1798 .expect_err("a non-loopback http base URL must be rejected");
1799
1800 assert!(err.is_validation(), "expected a validation error, got {err}");
1801 let message = err.to_string();
1802 assert!(
1803 message.contains("unencrypted"),
1804 "error should explain the cleartext risk, got: {message}"
1805 );
1806 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
1807 }
1808
1809 #[test]
1810 fn with_base_url_rejects_non_http_schemes_and_garbage() {
1811 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
1812 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1813 .unwrap()
1814 .with_base_url(url.to_string())
1815 .expect_err("non-https, non-loopback base URL must be rejected");
1816 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
1817 }
1818 }
1819
1820 #[test]
1827 fn env_base_url_absent_falls_back_to_default() {
1828 let resolved = Anthropic::resolve_base_url(None).unwrap();
1829 assert_eq!(resolved, DEFAULT_API_URL);
1830 }
1831
1832 #[test]
1833 fn env_base_url_accepts_https() {
1834 let resolved =
1835 Anthropic::resolve_base_url(Some("https://gateway.example.com/anthropic".to_string()))
1836 .unwrap();
1837 assert_eq!(resolved, "https://gateway.example.com/anthropic");
1838 }
1839
1840 #[test]
1841 fn env_base_url_allows_loopback_http_for_local_dev() {
1842 for url in ["http://localhost:11434", "http://127.0.0.1:11434", "http://[::1]:11434"] {
1843 let resolved = Anthropic::resolve_base_url(Some(url.to_string()))
1844 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
1845 assert_eq!(resolved, url);
1846 }
1847 }
1848
1849 #[test]
1850 fn env_base_url_rejects_cleartext_http() {
1851 let err =
1852 Anthropic::resolve_base_url(Some("http://gateway.internal.example.com".to_string()))
1853 .expect_err("a non-loopback http ANTHROPIC_BASE_URL must be rejected");
1854
1855 assert!(err.is_validation(), "expected a validation error, got {err}");
1856 let message = err.to_string();
1857 assert!(
1858 message.contains("unencrypted"),
1859 "error should explain the cleartext risk, got: {message}"
1860 );
1861 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
1862 }
1863
1864 #[test]
1865 fn env_base_url_rejects_non_http_schemes_and_garbage() {
1866 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
1867 let err = Anthropic::resolve_base_url(Some(url.to_string()))
1868 .expect_err("non-https, non-loopback ANTHROPIC_BASE_URL must be rejected");
1869 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
1870 }
1871 }
1872
1873 #[test]
1874 fn with_base_url_and_timeout_rejects_cleartext_http() {
1875 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
1876 .unwrap()
1877 .with_base_url_and_timeout(
1878 "http://gateway.internal.example.com".to_string(),
1879 Duration::from_secs(5),
1880 )
1881 .expect_err("a non-loopback http base URL must be rejected");
1882 assert!(err.is_validation(), "expected a validation error, got {err}");
1883 }
1884
1885 #[test]
1886 fn client_timeout_configuration() {
1887 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1888 let timeout = Duration::from_secs(30);
1889
1890 let configured_client = client.with_timeout(timeout).unwrap();
1891 assert_eq!(configured_client.timeout, timeout);
1892 }
1893
1894 #[test]
1895 fn client_cached_headers_performance() {
1896 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1897
1898 let headers1 = client.default_headers();
1900 let headers2 = client.default_headers();
1901
1902 assert_eq!(headers1.len(), headers2.len());
1903 assert!(headers1.contains_key("x-api-key"));
1904 assert!(headers1.contains_key("anthropic-version"));
1905 assert!(headers1.contains_key("content-type"));
1906 }
1907
1908 #[test]
1909 fn request_error_mapping() {
1910 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
1911
1912 let _timeout = Duration::from_secs(30);
1915 assert_eq!(client.timeout, DEFAULT_TIMEOUT); }
1917
1918 #[tokio::test]
1919 async fn concurrent_retry_safety() {
1920 use std::sync::atomic::{AtomicUsize, Ordering};
1921 use tokio::spawn;
1922
1923 let client = Anthropic {
1924 api_key: "test".to_string(),
1925 client: ReqwestClient::new(),
1926 base_url: "http://localhost".to_string(),
1927 timeout: Duration::from_secs(1),
1928 max_retries: 1,
1929 throughput_ops_sec: 1.0,
1930 reserve_capacity: 1.0,
1931 cached_headers: Arc::new(HeaderMap::new()),
1932 };
1933
1934 let attempt_counter = Arc::new(AtomicUsize::new(0));
1935 let mut handles = vec![];
1936
1937 for _ in 0..3 {
1939 let client_clone = client.clone();
1940 let counter_clone = attempt_counter.clone();
1941
1942 let handle = spawn(async move {
1943 client_clone
1944 .retry_with_backoff(|| {
1945 let counter = counter_clone.clone();
1946 async move {
1947 counter.fetch_add(1, Ordering::SeqCst);
1948 Ok::<String, Error>("success".to_string())
1949 }
1950 })
1951 .await
1952 });
1953 handles.push(handle);
1954 }
1955
1956 for handle in handles {
1958 let result = handle.await.unwrap();
1959 assert!(result.is_ok());
1960 }
1961
1962 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1964 }
1965}