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::{InsecureHttp, 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";
90const CODE_INSECURE_HTTP_OPT_IN: &str =
92 "call `allow_insecure_http()` on the client before setting the base URL";
93const ENV_INSECURE_HTTP_OPT_IN: &str =
95 "set ANTHROPIC_ALLOW_INSECURE_HTTP=1 alongside ANTHROPIC_BASE_URL";
96
97#[derive(Clone)]
109pub struct Anthropic {
110 api_key: String,
111 client: ReqwestClient,
112 stream_client: ReqwestClient,
114 base_url: String,
115 timeout: Duration,
116 stream_timeout: Option<Duration>,
117 max_retries: usize,
118 throughput_ops_sec: f64,
119 reserve_capacity: f64,
120 cached_headers: Arc<HeaderMap>,
122 allow_insecure_http: bool,
124}
125
126impl std::fmt::Debug for Anthropic {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("Anthropic")
129 .field("api_key", &"[REDACTED]")
130 .field("client", &self.client)
131 .field("stream_client", &self.stream_client)
132 .field("base_url", &self.base_url)
133 .field("timeout", &self.timeout)
134 .field("stream_timeout", &self.stream_timeout)
135 .field("max_retries", &self.max_retries)
136 .field("throughput_ops_sec", &self.throughput_ops_sec)
137 .field("reserve_capacity", &self.reserve_capacity)
138 .field("cached_headers", &self.cached_headers)
139 .field("allow_insecure_http", &self.allow_insecure_http)
140 .finish()
141 }
142}
143
144impl Anthropic {
145 fn build_http_client(
148 total_timeout: Option<Duration>,
149 connect_timeout: Duration,
150 ) -> Result<ReqwestClient> {
151 let mut builder = ReqwestClient::builder()
152 .connect_timeout(connect_timeout)
153 .redirect(reqwest::redirect::Policy::none())
154 .pool_max_idle_per_host(10) .pool_idle_timeout(Duration::from_secs(90))
156 .tcp_keepalive(Duration::from_secs(60));
157 if let Some(timeout) = total_timeout {
158 builder = builder.timeout(timeout);
159 }
160 builder.build().map_err(|e| {
161 Error::http_client(format!("Failed to build HTTP client: {e}"), Some(Box::new(e)))
162 })
163 }
164
165 fn resolve_api_key(key_value: &str) -> Result<String> {
167 if let Some(stripped) = key_value.strip_prefix("file://") {
168 let path = if stripped.starts_with('/') {
170 stripped.to_string()
172 } else {
173 stripped.to_string()
175 };
176
177 fs::read_to_string(&path).map(|content| content.trim().to_string()).map_err(|e| {
178 Error::validation(
179 format!("Failed to read API key from file '{}': {}", path, e),
180 Some("api_key".to_string()),
181 )
182 })
183 } else {
184 Ok(key_value.to_string())
186 }
187 }
188
189 fn resolve_base_url(
205 env_value: Option<String>,
206 insecure_http_env: Option<String>,
207 ) -> Result<String> {
208 match env_value {
209 Some(value) => {
210 let acknowledged = insecure_http_env
211 .as_deref()
212 .map(str::trim)
213 .is_some_and(|flag| flag == "1" || flag.eq_ignore_ascii_case("true"));
214 let insecure_http = if acknowledged {
215 InsecureHttp::Acknowledged
216 } else {
217 InsecureHttp::Rejected { opt_in: Some(ENV_INSECURE_HTTP_OPT_IN) }
218 };
219 validate_base_url(&value, insecure_http)?;
220 Ok(value)
221 }
222 None => Ok(DEFAULT_API_URL.to_string()),
223 }
224 }
225
226 pub fn new(api_key: Option<String>) -> Result<Self> {
251 let api_key = match api_key {
252 Some(key) => Self::resolve_api_key(&key)?,
253 None => {
254 let env_key = env::var("ANTHROPIC_API_KEY").map_err(|_| {
255 Error::authentication(
256 "API key not provided and ANTHROPIC_API_KEY environment variable not set",
257 )
258 })?;
259 Self::resolve_api_key(&env_key)?
260 }
261 };
262
263 let base_url = Self::resolve_base_url(
264 env::var("ANTHROPIC_BASE_URL").ok(),
265 env::var("ANTHROPIC_ALLOW_INSECURE_HTTP").ok(),
266 )?;
267 Self::from_values(api_key, base_url)
268 }
269
270 pub fn new_with_base_url(
295 api_key: impl Into<String>,
296 base_url: impl Into<String>,
297 ) -> Result<Self> {
298 let base_url = base_url.into();
299 validate_base_url(
300 &base_url,
301 InsecureHttp::Rejected { opt_in: Some(CODE_INSECURE_HTTP_OPT_IN) },
302 )?;
303 Self::from_values(Self::resolve_api_key(&api_key.into())?, base_url)
304 }
305
306 fn from_values(api_key: String, base_url: String) -> Result<Self> {
307 let timeout = DEFAULT_TIMEOUT;
308 let client = Self::build_http_client(Some(timeout), timeout)?;
309 let stream_client = Self::build_http_client(None, timeout)?;
310
311 let cached_headers = Arc::new(Self::build_default_headers(&api_key)?);
313
314 Ok(Self {
315 api_key,
316 client,
317 stream_client,
318 base_url,
319 timeout,
320 stream_timeout: None,
321 max_retries: 3,
322 throughput_ops_sec: 1.0 / 60.0,
323 reserve_capacity: 1.0 / 60.0,
324 cached_headers,
325 allow_insecure_http: false,
326 })
327 }
328
329 pub fn new_with_auth_token(auth_token: impl Into<String>) -> Result<Self> {
340 Self::new(Some(String::new()))?.with_auth_token(auth_token)
341 }
342
343 pub fn with_auth_token(mut self, auth_token: impl Into<String>) -> Result<Self> {
352 let auth_token = auth_token.into();
353 if auth_token.trim().is_empty() {
354 return Err(Error::validation(
355 "Auth token cannot be empty".to_string(),
356 Some("auth_token".to_string()),
357 ));
358 }
359
360 let mut headers = (*self.cached_headers).clone();
361 headers.remove("x-api-key");
362 let mut value =
363 HeaderValue::from_str(&format!("Bearer {auth_token}")).map_err(|error| {
364 Error::validation(
365 format!("Invalid auth token format: {error}"),
366 Some("auth_token".to_string()),
367 )
368 })?;
369 value.set_sensitive(true);
370 headers.insert(header::AUTHORIZATION, value);
371 self.api_key.clear();
372 self.cached_headers = Arc::new(headers);
373 Ok(self)
374 }
375
376 pub fn with_api_version(mut self, api_version: impl Into<String>) -> Result<Self> {
383 let api_version = api_version.into();
384 if api_version.trim().is_empty() {
385 return Err(Error::validation(
386 "API version cannot be empty".to_string(),
387 Some("api_version".to_string()),
388 ));
389 }
390
391 let mut headers = (*self.cached_headers).clone();
392 let value = HeaderValue::from_str(&api_version).map_err(|error| {
393 Error::validation(
394 format!("Invalid API version format: {error}"),
395 Some("api_version".to_string()),
396 )
397 })?;
398 headers.insert("anthropic-version", value);
399 self.cached_headers = Arc::new(headers);
400 Ok(self)
401 }
402
403 pub fn allow_insecure_http(mut self) -> Self {
430 self.allow_insecure_http = true;
431 self
432 }
433
434 pub fn with_base_url(mut self, base_url: String) -> Result<Self> {
470 let insecure_http = if self.allow_insecure_http {
471 InsecureHttp::Acknowledged
472 } else {
473 InsecureHttp::Rejected { opt_in: Some(CODE_INSECURE_HTTP_OPT_IN) }
474 };
475 validate_base_url(&base_url, insecure_http)?;
476 self.base_url = base_url;
477 Ok(self)
478 }
479
480 pub fn base_url(&self) -> &str {
482 &self.base_url
483 }
484
485 pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
495 self.timeout = timeout;
496 self.client = Self::build_http_client(Some(timeout), timeout)?;
497 self.stream_client = Self::build_http_client(None, timeout)?;
498 Ok(self)
499 }
500
501 pub fn with_stream_timeout(mut self, stream_timeout: Option<Duration>) -> Self {
518 self.stream_timeout = stream_timeout;
519 self
520 }
521
522 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
526 self.max_retries = max_retries;
527 self
528 }
529
530 pub fn api_key(&self) -> &str {
532 &self.api_key
533 }
534
535 pub fn with_backoff_params(mut self, throughput_ops_sec: f64, reserve_capacity: f64) -> Self {
539 self.throughput_ops_sec = throughput_ops_sec;
540 self.reserve_capacity = reserve_capacity;
541 self
542 }
543
544 pub fn with_base_url_and_timeout(self, base_url: String, timeout: Duration) -> Result<Self> {
573 self.with_base_url(base_url)?.with_timeout(timeout)
574 }
575
576 fn build_default_headers(api_key: &str) -> Result<HeaderMap> {
578 let mut headers = HeaderMap::new();
579 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
580 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
581 let mut api_key_value = HeaderValue::from_str(api_key).map_err(|e| {
582 Error::validation(format!("Invalid API key format: {e}"), Some("api_key".to_string()))
583 })?;
584 api_key_value.set_sensitive(true);
586 headers.insert("x-api-key", api_key_value);
587 headers.insert("anthropic-version", HeaderValue::from_static(ANTHROPIC_API_VERSION));
588 Ok(headers)
589 }
590
591 fn default_headers(&self) -> HeaderMap {
593 (*self.cached_headers).clone()
594 }
595
596 #[must_use]
614 pub fn with_default_headers(mut self, headers: HeaderMap) -> Self {
615 Arc::make_mut(&mut self.cached_headers).extend(headers);
616 self
617 }
618
619 pub fn default_headers_for_request(&self) -> HeaderMap {
626 self.default_headers()
627 }
628
629 fn build_url(&self, endpoint: &str) -> String {
642 let base = self.base_url.trim_end_matches('/');
643 format!("{}/v1/{}", base, endpoint)
644 }
645
646 async fn retry_with_backoff<F, Fut, T>(&self, operation: F) -> Result<T>
648 where
649 F: Fn() -> Fut,
650 Fut: std::future::Future<Output = Result<T>>,
651 {
652 let backoff = ExponentialBackoff::new(self.throughput_ops_sec, self.reserve_capacity);
653 let mut last_error = None;
654
655 for attempt in 0..=self.max_retries {
656 match operation().await {
657 Ok(result) => return Ok(result),
658 Err(error) => {
659 if !error.is_retryable() {
661 return Err(error);
662 }
663
664 if attempt == self.max_retries {
666 last_error = Some(error);
667 break;
668 }
669
670 let exp_backoff_duration = backoff.next();
672
673 let header_backoff_duration = match &error {
675 Error::RateLimit { retry_after: Some(seconds), .. } => {
676 Some(Duration::from_secs(*seconds))
677 }
678 Error::ServiceUnavailable { retry_after: Some(seconds), .. } => {
679 Some(Duration::from_secs(*seconds))
680 }
681 _ => None,
682 };
683
684 let sleep_duration = match header_backoff_duration {
686 Some(header_duration) => exp_backoff_duration.max(header_duration),
687 None => exp_backoff_duration,
688 };
689
690 CLIENT_REQUEST_RETRIES.click();
691 CLIENT_RETRY_BACKOFF.add(sleep_duration.as_secs_f64());
692 sleep(sleep_duration).await;
693 last_error = Some(error);
694 }
695 }
696 }
697
698 Err(last_error
699 .unwrap_or_else(|| Error::unknown("Failed after retries without capturing error")))
700 }
701
702 async fn process_error_response(response: Response) -> Error {
704 let status = response.status();
705 let status_code = status.as_u16();
706
707 let request_id = response
709 .headers()
710 .get("x-request-id")
711 .and_then(|val| val.to_str().ok())
712 .map(String::from);
713
714 let retry_after = response
715 .headers()
716 .get("retry-after")
717 .and_then(|val| val.to_str().ok())
718 .and_then(|val| val.parse::<u64>().ok());
719
720 #[derive(Deserialize)]
722 struct ErrorResponse {
723 error: Option<ErrorDetail>,
724 }
725
726 #[derive(Deserialize)]
727 struct ErrorDetail {
728 #[serde(rename = "type")]
729 error_type: Option<String>,
730 message: Option<String>,
731 param: Option<String>,
732 }
733
734 let error_body = match response.text().await {
735 Ok(body) => body,
736 Err(e) => {
737 return Error::http_client(
738 format!("Failed to read error response: {e}"),
739 Some(Box::new(e)),
740 );
741 }
742 };
743
744 let parsed_error = serde_json::from_str::<ErrorResponse>(&error_body).ok();
746 let error_type =
747 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.error_type.clone());
748 let error_message = parsed_error
749 .as_ref()
750 .and_then(|e| e.error.as_ref())
751 .and_then(|e| e.message.clone())
752 .unwrap_or_else(|| error_body.clone());
753 let error_param =
754 parsed_error.as_ref().and_then(|e| e.error.as_ref()).and_then(|e| e.param.clone());
755
756 match status_code {
758 400 => Error::bad_request(error_message, error_param),
759 401 => Error::authentication(error_message),
760 403 => Error::permission(error_message),
761 404 => Error::not_found(error_message, None, None),
762 408 => Error::timeout(error_message, None),
763 429 => Error::rate_limit(error_message, retry_after),
764 500 => Error::internal_server(error_message, request_id),
765 502..=504 => Error::service_unavailable(error_message, retry_after),
766 529 => Error::rate_limit(error_message, retry_after),
767 _ => Error::api(status_code, error_type, error_message, request_id),
768 }
769 }
770
771 fn map_request_error(&self, e: reqwest::Error) -> Error {
773 if e.is_timeout() {
774 Error::timeout(format!("Request timed out: {e}"), Some(self.timeout.as_secs_f64()))
775 } else if e.is_connect() {
776 Error::connection(format!("Connection error: {e}"), Some(Box::new(e)))
777 } else {
778 Error::http_client(format!("Request failed: {e}"), Some(Box::new(e)))
779 }
780 }
781
782 async fn execute_post_request<T: serde::de::DeserializeOwned>(
784 &self,
785 url: &str,
786 body: &impl serde::Serialize,
787 headers: Option<HeaderMap>,
788 ) -> Result<T> {
789 let headers = headers.unwrap_or_else(|| self.default_headers());
790
791 let response = self
792 .client
793 .post(url)
794 .headers(headers)
795 .json(body)
796 .send()
797 .await
798 .map_err(|e| self.map_request_error(e))?;
799
800 if !response.status().is_success() {
801 return Err(Self::process_error_response(response).await);
802 }
803
804 response.json::<T>().await.map_err(|e| {
805 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
806 })
807 }
808
809 async fn send_stream_request(
815 &self,
816 url: &str,
817 headers: HeaderMap,
818 body: &impl serde::Serialize,
819 ) -> Result<Response> {
820 let mut request = self.stream_client.post(url).headers(headers).json(body);
821 if let Some(stream_timeout) = self.stream_timeout {
822 request = request.timeout(stream_timeout);
823 }
824
825 let response = tokio::time::timeout(self.timeout, request.send())
826 .await
827 .map_err(|_| {
828 Error::timeout(
829 format!(
830 "Timed out after {} seconds waiting for the streaming response headers",
831 self.timeout.as_secs_f64()
832 ),
833 Some(self.timeout.as_secs_f64()),
834 )
835 })?
836 .map_err(|e| self.map_request_error(e))?;
837
838 if !response.status().is_success() {
839 return Err(Self::process_error_response(response).await);
840 }
841 Ok(response)
842 }
843
844 async fn execute_get_request<T: serde::de::DeserializeOwned>(
846 &self,
847 url: &str,
848 query_params: Option<&[(String, String)]>,
849 ) -> Result<T> {
850 let mut request = self.client.get(url).headers(self.default_headers());
851
852 if let Some(params) = query_params {
853 for (key, value) in params {
854 request = request.query(&[(key, value)]);
855 }
856 }
857
858 let response = request.send().await.map_err(|e| self.map_request_error(e))?;
859
860 if !response.status().is_success() {
861 return Err(Self::process_error_response(response).await);
862 }
863
864 response.json::<T>().await.map_err(|e| {
865 Error::serialization(format!("Failed to parse response: {e}"), Some(Box::new(e)))
866 })
867 }
868
869 fn append_beta_header(headers: &mut HeaderMap, beta: &str) -> Result<()> {
870 if beta.trim().is_empty() {
871 return Err(Error::validation(
872 "Beta identifier cannot be empty".to_string(),
873 Some("anthropic-beta".to_string()),
874 ));
875 }
876 let existing = headers
877 .get("anthropic-beta")
878 .map(HeaderValue::to_str)
879 .transpose()
880 .map_err(|error| {
881 Error::validation(
882 format!("Invalid existing anthropic-beta header: {error}"),
883 Some("anthropic-beta".to_string()),
884 )
885 })?
886 .unwrap_or_default();
887 if existing.split(',').any(|value| value.trim() == beta) {
888 return Ok(());
889 }
890
891 let combined =
892 if existing.is_empty() { beta.to_string() } else { format!("{existing},{beta}") };
893 let value = HeaderValue::from_str(&combined).map_err(|error| {
894 Error::validation(
895 format!("Invalid anthropic-beta header: {error}"),
896 Some("anthropic-beta".to_string()),
897 )
898 })?;
899 headers.insert("anthropic-beta", value);
900 Ok(())
901 }
902
903 fn message_headers(
904 &self,
905 params: &MessageCreateParams,
906 extra_betas: &[&str],
907 streaming: bool,
908 ) -> Result<HeaderMap> {
909 let mut headers = self.default_headers();
910 if streaming {
911 headers.insert(header::ACCEPT, HeaderValue::from_static("text/event-stream"));
912 }
913 if params.requires_structured_outputs_beta() {
914 Self::append_beta_header(&mut headers, STRUCTURED_OUTPUTS_BETA)?;
915 }
916 if params.context_management.is_some() {
917 Self::append_beta_header(&mut headers, "context-management-2025-06-27")?;
918 }
919 if params.speed.is_some() {
920 Self::append_beta_header(&mut headers, "fast-mode-2026-02-01")?;
921 }
922 for beta in extra_betas {
923 Self::append_beta_header(&mut headers, beta)?;
924 }
925 Ok(headers)
926 }
927
928 async fn send_with_resolved_headers(
929 &self,
930 mut params: MessageCreateParams,
931 replacement_headers: Option<HeaderMap>,
932 ) -> Result<Message> {
933 let start = Instant::now();
934 CLIENT_REQUESTS.click();
935
936 if let Err(err) = params.validate() {
938 CLIENT_REQUEST_ERRORS.click();
939 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
940 return Err(err);
941 }
942
943 params.stream = false;
945
946 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
948 params.temperature = Some(1.0);
949 }
950
951 let headers = match replacement_headers {
952 Some(headers) => headers,
953 None => self.message_headers(¶ms, &[], false)?,
954 };
955
956 let result = self
957 .retry_with_backoff(|| async {
958 let url = self.build_url("messages");
959 self.execute_post_request(&url, ¶ms, Some(headers.clone())).await
960 })
961 .await;
962
963 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
964 if result.is_err() {
965 CLIENT_REQUEST_ERRORS.click();
966 }
967 result
968 }
969
970 pub async fn send(&self, params: MessageCreateParams) -> Result<Message> {
972 self.send_with_resolved_headers(params, None).await
973 }
974
975 pub async fn send_with_headers(
988 &self,
989 params: MessageCreateParams,
990 headers: HeaderMap,
991 ) -> Result<Message> {
992 self.send_with_resolved_headers(params, Some(headers)).await
993 }
994
995 pub async fn send_with_betas(
1006 &self,
1007 params: MessageCreateParams,
1008 betas: &[&str],
1009 ) -> Result<Message> {
1010 let headers = self.message_headers(¶ms, betas, false)?;
1011 self.send_with_resolved_headers(params, Some(headers)).await
1012 }
1013
1014 pub async fn send_with_logger(
1019 &self,
1020 params: MessageCreateParams,
1021 logger: &dyn ClientLogger,
1022 ) -> Result<Message> {
1023 let result = self.send(params).await;
1024 if let Ok(ref message) = result {
1025 logger.log_response(message);
1026 }
1027 result
1028 }
1029
1030 async fn stream_with_resolved_headers(
1034 &self,
1035 params: &MessageCreateParams,
1036 replacement_headers: Option<HeaderMap>,
1037 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
1038 let start = Instant::now();
1039 CLIENT_REQUESTS.click();
1040
1041 if let Err(err) = params.validate() {
1043 CLIENT_REQUEST_ERRORS.click();
1044 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1045 return Err(err);
1046 }
1047
1048 let mut params = params.clone();
1050 params.stream = true;
1051
1052 if matches!(params.thinking, Some(ThinkingConfig::Enabled { .. })) {
1054 params.temperature = Some(1.0);
1055 }
1056
1057 let headers = match replacement_headers {
1058 Some(headers) => headers,
1059 None => self.message_headers(¶ms, &[], true)?,
1060 };
1061
1062 let response = self
1063 .retry_with_backoff(|| async {
1064 let url = self.build_url("messages");
1065 self.send_stream_request(&url, headers.clone(), ¶ms).await
1066 })
1067 .await;
1068
1069 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1070 let response = match response {
1071 Ok(response) => response,
1072 Err(err) => {
1073 CLIENT_REQUEST_ERRORS.click();
1074 return Err(err);
1075 }
1076 };
1077
1078 let stream = response.bytes_stream();
1080
1081 Ok(process_sse(stream))
1083 }
1084
1085 pub async fn stream(
1087 &self,
1088 params: &MessageCreateParams,
1089 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
1090 self.stream_with_resolved_headers(params, None).await
1091 }
1092
1093 pub async fn stream_with_headers(
1104 &self,
1105 params: &MessageCreateParams,
1106 headers: HeaderMap,
1107 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
1108 self.stream_with_resolved_headers(params, Some(headers)).await
1109 }
1110
1111 pub async fn stream_with_betas(
1122 &self,
1123 params: &MessageCreateParams,
1124 betas: &[&str],
1125 ) -> Result<impl Stream<Item = Result<MessageStreamEvent>> + use<>> {
1126 let headers = self.message_headers(params, betas, true)?;
1127 self.stream_with_resolved_headers(params, Some(headers)).await
1128 }
1129
1130 pub async fn send_with_server_fallbacks(
1140 &self,
1141 mut request: ServerFallbackRequest,
1142 ) -> Result<ServerFallbackMessage> {
1143 let start = Instant::now();
1144 CLIENT_REQUESTS.click();
1145 if let Err(error) = request.validate() {
1146 CLIENT_REQUEST_ERRORS.click();
1147 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1148 return Err(error);
1149 }
1150
1151 request.params.stream = false;
1152 if matches!(request.params.thinking, Some(ThinkingConfig::Enabled { .. })) {
1153 request.params.temperature = Some(1.0);
1154 }
1155 let headers = self.message_headers(&request.params, &[SERVER_FALLBACK_BETA], false)?;
1156 let result = self
1157 .retry_with_backoff(|| async {
1158 let url = self.build_url("messages");
1159 self.execute_post_request(&url, &request, Some(headers.clone())).await
1160 })
1161 .await;
1162
1163 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1164 if result.is_err() {
1165 CLIENT_REQUEST_ERRORS.click();
1166 }
1167 result
1168 }
1169
1170 pub async fn stream_with_server_fallbacks(
1177 &self,
1178 request: &ServerFallbackRequest,
1179 ) -> Result<impl Stream<Item = Result<ServerFallbackStreamEvent>> + use<>> {
1180 let start = Instant::now();
1181 CLIENT_REQUESTS.click();
1182 if let Err(error) = request.validate() {
1183 CLIENT_REQUEST_ERRORS.click();
1184 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1185 return Err(error);
1186 }
1187
1188 let mut request = request.clone();
1189 request.params.stream = true;
1190 if matches!(request.params.thinking, Some(ThinkingConfig::Enabled { .. })) {
1191 request.params.temperature = Some(1.0);
1192 }
1193 let headers = self.message_headers(&request.params, &[SERVER_FALLBACK_BETA], true)?;
1194 let response = self
1195 .retry_with_backoff(|| async {
1196 let url = self.build_url("messages");
1197 self.send_stream_request(&url, headers.clone(), &request).await
1198 })
1199 .await;
1200
1201 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1202 let response = match response {
1203 Ok(response) => response,
1204 Err(error) => {
1205 CLIENT_REQUEST_ERRORS.click();
1206 return Err(error);
1207 }
1208 };
1209 Ok(process_json_sse(response.bytes_stream()))
1210 }
1211
1212 pub async fn stream_with_logger<'a>(
1222 &self,
1223 params: &MessageCreateParams,
1224 logger: &'a dyn ClientLogger,
1225 ) -> Result<LoggingStream<'a>> {
1226 let raw_stream = self.stream(params).await?;
1227 let (accumulating_stream, receiver) = AccumulatingStream::new(raw_stream);
1228 Ok(LoggingStream::new(accumulating_stream, receiver, logger))
1229 }
1230
1231 pub async fn count_tokens(
1236 &self,
1237 params: MessageCountTokensParams,
1238 ) -> Result<MessageTokensCount> {
1239 let start = Instant::now();
1240 CLIENT_REQUESTS.click();
1241 let result = self
1242 .retry_with_backoff(|| async {
1243 let url = self.build_url("messages/count_tokens");
1244 self.execute_post_request(&url, ¶ms, None).await
1245 })
1246 .await;
1247
1248 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1249 if result.is_err() {
1250 CLIENT_REQUEST_ERRORS.click();
1251 }
1252 result
1253 }
1254
1255 pub async fn list_models(&self, params: Option<ModelListParams>) -> Result<ModelListResponse> {
1260 let start = Instant::now();
1261 CLIENT_REQUESTS.click();
1262 let result = self
1263 .retry_with_backoff(|| async {
1264 let url = self.build_url("models");
1265
1266 let query_params = params.as_ref().map(|p| {
1267 let mut params = Vec::new();
1268 if let Some(ref after_id) = p.after_id {
1269 params.push(("after_id".to_string(), after_id.clone()));
1270 }
1271 if let Some(ref before_id) = p.before_id {
1272 params.push(("before_id".to_string(), before_id.clone()));
1273 }
1274 if let Some(limit) = p.limit {
1275 params.push(("limit".to_string(), limit.to_string()));
1276 }
1277 params
1278 });
1279
1280 self.execute_get_request(&url, query_params.as_deref()).await
1281 })
1282 .await;
1283
1284 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1285 if result.is_err() {
1286 CLIENT_REQUEST_ERRORS.click();
1287 }
1288 result
1289 }
1290
1291 pub async fn get_model(&self, model_id: &str) -> Result<ModelInfo> {
1296 let start = Instant::now();
1297 CLIENT_REQUESTS.click();
1298 let result = self
1299 .retry_with_backoff(|| async {
1300 let url = self.build_url(&format!("models/{}", model_id));
1301 self.execute_get_request(&url, None).await
1302 })
1303 .await;
1304
1305 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1306 if result.is_err() {
1307 CLIENT_REQUEST_ERRORS.click();
1308 }
1309 result
1310 }
1311
1312 async fn execute_delete_request(&self, url: &str) -> Result<()> {
1316 let response = self
1317 .client
1318 .delete(url)
1319 .headers(self.default_headers())
1320 .send()
1321 .await
1322 .map_err(|e| self.map_request_error(e))?;
1323
1324 if !response.status().is_success() {
1325 return Err(Self::process_error_response(response).await);
1326 }
1327
1328 Ok(())
1329 }
1330
1331 fn pagination_params(
1333 before_id: Option<&str>,
1334 after_id: Option<&str>,
1335 limit: Option<u32>,
1336 ) -> Option<Vec<(String, String)>> {
1337 let mut params = Vec::new();
1338 if let Some(before) = before_id {
1339 params.push(("before_id".to_string(), before.to_string()));
1340 }
1341 if let Some(after) = after_id {
1342 params.push(("after_id".to_string(), after.to_string()));
1343 }
1344 if let Some(lim) = limit {
1345 params.push(("limit".to_string(), lim.to_string()));
1346 }
1347 if params.is_empty() { None } else { Some(params) }
1348 }
1349
1350 pub async fn create_batch(&self, requests: Vec<BatchRequest>) -> Result<MessageBatch> {
1354 let start = Instant::now();
1355 CLIENT_REQUESTS.click();
1356 let body = serde_json::json!({ "requests": requests });
1357 let result = self
1358 .retry_with_backoff(|| async {
1359 let url = self.build_url("messages/batches");
1360 self.execute_post_request(&url, &body, None).await
1361 })
1362 .await;
1363 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1364 if result.is_err() {
1365 CLIENT_REQUEST_ERRORS.click();
1366 }
1367 result
1368 }
1369
1370 pub async fn get_batch(&self, batch_id: &str) -> Result<MessageBatch> {
1372 let start = Instant::now();
1373 CLIENT_REQUESTS.click();
1374 let result = self
1375 .retry_with_backoff(|| async {
1376 let url = self.build_url(&format!("messages/batches/{batch_id}"));
1377 self.execute_get_request(&url, None).await
1378 })
1379 .await;
1380 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1381 if result.is_err() {
1382 CLIENT_REQUEST_ERRORS.click();
1383 }
1384 result
1385 }
1386
1387 pub async fn batch_results(&self, batch_id: &str) -> Result<Vec<BatchResultItem>> {
1389 let start = Instant::now();
1390 CLIENT_REQUESTS.click();
1391 let result = self
1392 .retry_with_backoff(|| async {
1393 let url = self.build_url(&format!("messages/batches/{batch_id}/results"));
1394 let response = self
1395 .client
1396 .get(&url)
1397 .headers(self.default_headers())
1398 .send()
1399 .await
1400 .map_err(|e| self.map_request_error(e))?;
1401
1402 if !response.status().is_success() {
1403 return Err(Self::process_error_response(response).await);
1404 }
1405
1406 let text = response.text().await.map_err(|e| {
1407 Error::serialization(
1408 format!("Failed to read batch results: {e}"),
1409 Some(Box::new(e)),
1410 )
1411 })?;
1412
1413 let mut items = Vec::new();
1414 for line in text.lines() {
1415 let trimmed = line.trim();
1416 if trimmed.is_empty() {
1417 continue;
1418 }
1419 let item: BatchResultItem = serde_json::from_str(trimmed)?;
1420 items.push(item);
1421 }
1422 Ok(items)
1423 })
1424 .await;
1425 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1426 if result.is_err() {
1427 CLIENT_REQUEST_ERRORS.click();
1428 }
1429 result
1430 }
1431
1432 pub async fn cancel_batch(&self, batch_id: &str) -> Result<MessageBatch> {
1434 let start = Instant::now();
1435 CLIENT_REQUESTS.click();
1436 let result = self
1437 .retry_with_backoff(|| async {
1438 let url = self.build_url(&format!("messages/batches/{batch_id}/cancel"));
1439 self.execute_post_request(&url, &serde_json::json!({}), None).await
1440 })
1441 .await;
1442 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1443 if result.is_err() {
1444 CLIENT_REQUEST_ERRORS.click();
1445 }
1446 result
1447 }
1448
1449 pub async fn delete_batch(&self, batch_id: &str) -> Result<()> {
1451 let start = Instant::now();
1452 CLIENT_REQUESTS.click();
1453 let result = self
1454 .retry_with_backoff(|| async {
1455 let url = self.build_url(&format!("messages/batches/{batch_id}"));
1456 self.execute_delete_request(&url).await
1457 })
1458 .await;
1459 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1460 if result.is_err() {
1461 CLIENT_REQUEST_ERRORS.click();
1462 }
1463 result
1464 }
1465
1466 pub async fn list_batches(
1468 &self,
1469 before_id: Option<&str>,
1470 after_id: Option<&str>,
1471 limit: Option<u32>,
1472 ) -> Result<PaginatedList<MessageBatch>> {
1473 let start = Instant::now();
1474 CLIENT_REQUESTS.click();
1475 let result = self
1476 .retry_with_backoff(|| async {
1477 let url = self.build_url("messages/batches");
1478 let query = Self::pagination_params(before_id, after_id, limit);
1479 self.execute_get_request(&url, query.as_deref()).await
1480 })
1481 .await;
1482 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1483 if result.is_err() {
1484 CLIENT_REQUEST_ERRORS.click();
1485 }
1486 result
1487 }
1488
1489 pub async fn upload_file(
1493 &self,
1494 data: Vec<u8>,
1495 mime_type: &str,
1496 filename: &str,
1497 purpose: &str,
1498 ) -> Result<FileObject> {
1499 let start = Instant::now();
1500 CLIENT_REQUESTS.click();
1501
1502 let mime_type = mime_type.to_string();
1503 let filename = filename.to_string();
1504 let purpose = purpose.to_string();
1505
1506 let result = self
1507 .retry_with_backoff(|| {
1508 let data = data.clone();
1509 let mime_type = mime_type.clone();
1510 let filename = filename.clone();
1511 let purpose = purpose.clone();
1512 async move {
1513 let url = self.build_url("files");
1514 let part = reqwest::multipart::Part::bytes(data)
1515 .file_name(filename)
1516 .mime_str(&mime_type)
1517 .map_err(|e| {
1518 Error::validation(
1519 format!("Invalid MIME type: {e}"),
1520 Some("mime_type".to_string()),
1521 )
1522 })?;
1523 let form =
1524 reqwest::multipart::Form::new().text("purpose", purpose).part("file", part);
1525
1526 let response = self
1527 .client
1528 .post(&url)
1529 .headers(self.default_headers())
1530 .multipart(form)
1531 .send()
1532 .await
1533 .map_err(|e| self.map_request_error(e))?;
1534
1535 if !response.status().is_success() {
1536 return Err(Self::process_error_response(response).await);
1537 }
1538
1539 response.json::<FileObject>().await.map_err(|e| {
1540 Error::serialization(
1541 format!("Failed to parse file response: {e}"),
1542 Some(Box::new(e)),
1543 )
1544 })
1545 }
1546 })
1547 .await;
1548 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1549 if result.is_err() {
1550 CLIENT_REQUEST_ERRORS.click();
1551 }
1552 result
1553 }
1554
1555 pub async fn get_file(&self, file_id: &str) -> Result<FileObject> {
1557 let start = Instant::now();
1558 CLIENT_REQUESTS.click();
1559 let result = self
1560 .retry_with_backoff(|| async {
1561 let url = self.build_url(&format!("files/{file_id}"));
1562 self.execute_get_request(&url, None).await
1563 })
1564 .await;
1565 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1566 if result.is_err() {
1567 CLIENT_REQUEST_ERRORS.click();
1568 }
1569 result
1570 }
1571
1572 pub async fn delete_file(&self, file_id: &str) -> Result<()> {
1574 let start = Instant::now();
1575 CLIENT_REQUESTS.click();
1576 let result = self
1577 .retry_with_backoff(|| async {
1578 let url = self.build_url(&format!("files/{file_id}"));
1579 self.execute_delete_request(&url).await
1580 })
1581 .await;
1582 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1583 if result.is_err() {
1584 CLIENT_REQUEST_ERRORS.click();
1585 }
1586 result
1587 }
1588
1589 pub async fn list_files(
1591 &self,
1592 before_id: Option<&str>,
1593 after_id: Option<&str>,
1594 limit: Option<u32>,
1595 ) -> Result<PaginatedList<FileObject>> {
1596 let start = Instant::now();
1597 CLIENT_REQUESTS.click();
1598 let result = self
1599 .retry_with_backoff(|| async {
1600 let url = self.build_url("files");
1601 let query = Self::pagination_params(before_id, after_id, limit);
1602 self.execute_get_request(&url, query.as_deref()).await
1603 })
1604 .await;
1605 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1606 if result.is_err() {
1607 CLIENT_REQUEST_ERRORS.click();
1608 }
1609 result
1610 }
1611
1612 pub async fn create_skill(
1616 &self,
1617 name: &str,
1618 description: &str,
1619 content: Vec<u8>,
1620 ) -> Result<SkillObject> {
1621 let start = Instant::now();
1622 CLIENT_REQUESTS.click();
1623 let body = serde_json::json!({
1624 "name": name,
1625 "description": description,
1626 "content": base64_encode(&content),
1627 });
1628 let result = self
1629 .retry_with_backoff(|| async {
1630 let url = self.build_url("skills");
1631 self.execute_post_request(&url, &body, None).await
1632 })
1633 .await;
1634 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1635 if result.is_err() {
1636 CLIENT_REQUEST_ERRORS.click();
1637 }
1638 result
1639 }
1640
1641 pub async fn get_skill(&self, skill_id: &str) -> Result<SkillObject> {
1643 let start = Instant::now();
1644 CLIENT_REQUESTS.click();
1645 let result = self
1646 .retry_with_backoff(|| async {
1647 let url = self.build_url(&format!("skills/{skill_id}"));
1648 self.execute_get_request(&url, None).await
1649 })
1650 .await;
1651 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1652 if result.is_err() {
1653 CLIENT_REQUEST_ERRORS.click();
1654 }
1655 result
1656 }
1657
1658 pub async fn update_skill(&self, skill_id: &str, content: Vec<u8>) -> Result<SkillObject> {
1660 let start = Instant::now();
1661 CLIENT_REQUESTS.click();
1662 let body = serde_json::json!({
1663 "content": base64_encode(&content),
1664 });
1665 let result = self
1666 .retry_with_backoff(|| async {
1667 let url = self.build_url(&format!("skills/{skill_id}"));
1668 self.execute_post_request(&url, &body, None).await
1669 })
1670 .await;
1671 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1672 if result.is_err() {
1673 CLIENT_REQUEST_ERRORS.click();
1674 }
1675 result
1676 }
1677
1678 pub async fn delete_skill(&self, skill_id: &str) -> Result<()> {
1680 let start = Instant::now();
1681 CLIENT_REQUESTS.click();
1682 let result = self
1683 .retry_with_backoff(|| async {
1684 let url = self.build_url(&format!("skills/{skill_id}"));
1685 self.execute_delete_request(&url).await
1686 })
1687 .await;
1688 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1689 if result.is_err() {
1690 CLIENT_REQUEST_ERRORS.click();
1691 }
1692 result
1693 }
1694
1695 pub async fn list_skills(
1697 &self,
1698 before_id: Option<&str>,
1699 after_id: Option<&str>,
1700 limit: Option<u32>,
1701 ) -> Result<PaginatedList<SkillObject>> {
1702 let start = Instant::now();
1703 CLIENT_REQUESTS.click();
1704 let result = self
1705 .retry_with_backoff(|| async {
1706 let url = self.build_url("skills");
1707 let query = Self::pagination_params(before_id, after_id, limit);
1708 self.execute_get_request(&url, query.as_deref()).await
1709 })
1710 .await;
1711 CLIENT_REQUEST_DURATION.add(start.elapsed().as_secs_f64());
1712 if result.is_err() {
1713 CLIENT_REQUEST_ERRORS.click();
1714 }
1715 result
1716 }
1717}
1718
1719#[cfg(test)]
1720mod tests {
1721 use super::*;
1722 use std::sync::Arc;
1723 use std::sync::atomic::{AtomicUsize, Ordering};
1724
1725 #[test]
1726 fn debug_output_redacts_api_key_and_bearer_token() {
1727 let client = Anthropic::new(Some("sk-ant-secret-key-value".to_string())).unwrap();
1728 let debug = format!("{client:?}");
1729 assert!(!debug.contains("sk-ant-secret-key-value"), "api key leaked: {debug}");
1730 assert!(debug.contains("[REDACTED]"));
1731
1732 let client = Anthropic::new_with_auth_token("secret-bearer-token").unwrap();
1733 let debug = format!("{client:?}");
1734 assert!(!debug.contains("secret-bearer-token"), "bearer token leaked: {debug}");
1735 }
1736
1737 #[test]
1738 fn api_key_header_is_marked_sensitive() {
1739 let headers = Anthropic::build_default_headers("sk-ant-secret-key-value").unwrap();
1740
1741 assert!(headers.get("x-api-key").unwrap().is_sensitive());
1742 }
1743
1744 #[tokio::test]
1745 async fn retry_logic_with_backoff() {
1746 let client = Anthropic {
1747 api_key: "test".to_string(),
1748 client: ReqwestClient::new(),
1749 stream_client: ReqwestClient::new(),
1750 base_url: "http://localhost".to_string(),
1751 timeout: Duration::from_secs(1),
1752 stream_timeout: None,
1753 max_retries: 2,
1754 throughput_ops_sec: 1.0 / 60.0,
1755 reserve_capacity: 1.0 / 60.0,
1756 cached_headers: Arc::new(HeaderMap::new()),
1757 allow_insecure_http: false,
1758 };
1759
1760 let attempt_counter = Arc::new(AtomicUsize::new(0));
1761 let counter_clone = attempt_counter.clone();
1762
1763 let result = client
1764 .retry_with_backoff(|| {
1765 let counter = counter_clone.clone();
1766 async move {
1767 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1768 match attempt {
1769 0 | 1 => Err(Error::rate_limit("Rate limited", Some(1))),
1770 _ => Ok("success".to_string()),
1771 }
1772 }
1773 })
1774 .await;
1775
1776 assert!(result.is_ok());
1777 assert_eq!(result.unwrap(), "success");
1778 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1779 }
1780
1781 #[tokio::test]
1782 async fn retry_logic_with_non_retryable_error() {
1783 let client = Anthropic {
1784 api_key: "test".to_string(),
1785 client: ReqwestClient::new(),
1786 stream_client: ReqwestClient::new(),
1787 base_url: "http://localhost".to_string(),
1788 timeout: Duration::from_secs(1),
1789 stream_timeout: None,
1790 max_retries: 2,
1791 throughput_ops_sec: 1.0 / 60.0,
1792 reserve_capacity: 1.0 / 60.0,
1793 cached_headers: Arc::new(HeaderMap::new()),
1794 allow_insecure_http: false,
1795 };
1796
1797 let attempt_counter = Arc::new(AtomicUsize::new(0));
1798 let counter_clone = attempt_counter.clone();
1799
1800 let result: Result<String> = client
1801 .retry_with_backoff(|| {
1802 let counter = counter_clone.clone();
1803 async move {
1804 counter.fetch_add(1, Ordering::SeqCst);
1805 Err(Error::authentication("Invalid API key"))
1806 }
1807 })
1808 .await;
1809
1810 assert!(result.is_err());
1811 assert!(result.unwrap_err().is_authentication());
1812 assert_eq!(attempt_counter.load(Ordering::SeqCst), 1);
1814 }
1815
1816 #[tokio::test]
1817 async fn retry_logic_max_retries_exceeded() {
1818 let client = Anthropic {
1819 api_key: "test".to_string(),
1820 client: ReqwestClient::new(),
1821 stream_client: ReqwestClient::new(),
1822 base_url: "http://localhost".to_string(),
1823 timeout: Duration::from_secs(1),
1824 stream_timeout: None,
1825 max_retries: 2,
1826 throughput_ops_sec: 1.0 / 60.0,
1827 reserve_capacity: 1.0 / 60.0,
1828 cached_headers: Arc::new(HeaderMap::new()),
1829 allow_insecure_http: false,
1830 };
1831
1832 let attempt_counter = Arc::new(AtomicUsize::new(0));
1833 let counter_clone = attempt_counter.clone();
1834
1835 let result: Result<String> = client
1836 .retry_with_backoff(|| {
1837 let counter = counter_clone.clone();
1838 async move {
1839 counter.fetch_add(1, Ordering::SeqCst);
1840 Err(Error::rate_limit("Always rate limited", Some(1)))
1841 }
1842 })
1843 .await;
1844
1845 assert!(result.is_err());
1846 assert!(result.unwrap_err().is_rate_limit());
1847 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1849 }
1850
1851 #[tokio::test]
1852 async fn error_529_is_retryable() {
1853 let client = Anthropic {
1855 api_key: "test".to_string(),
1856 client: ReqwestClient::new(),
1857 stream_client: ReqwestClient::new(),
1858 base_url: "http://localhost".to_string(),
1859 timeout: Duration::from_secs(1),
1860 stream_timeout: None,
1861 max_retries: 2,
1862 throughput_ops_sec: 1.0 / 60.0,
1863 reserve_capacity: 1.0 / 60.0,
1864 cached_headers: Arc::new(HeaderMap::new()),
1865 allow_insecure_http: false,
1866 };
1867
1868 let attempt_counter = Arc::new(AtomicUsize::new(0));
1869 let counter_clone = attempt_counter.clone();
1870
1871 let result = client
1872 .retry_with_backoff(|| {
1873 let counter = counter_clone.clone();
1874 async move {
1875 let attempt = counter.fetch_add(1, Ordering::SeqCst);
1876 match attempt {
1877 0 | 1 => {
1878 Err(Error::api(
1880 529,
1881 Some("overloaded_error".to_string()),
1882 "Overloaded".to_string(),
1883 None,
1884 ))
1885 }
1886 _ => Ok("success".to_string()),
1887 }
1888 }
1889 })
1890 .await;
1891
1892 assert!(result.is_ok());
1893 assert_eq!(result.unwrap(), "success");
1894 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1896 }
1897
1898 #[test]
1899 fn error_529_mapped_correctly() {
1900 let error =
1902 Error::api(529, Some("overloaded_error".to_string()), "Overloaded".to_string(), None);
1903 assert!(error.is_retryable());
1904
1905 let rate_limit_error = Error::rate_limit("Overloaded", Some(5));
1907 assert!(rate_limit_error.is_retryable());
1908 }
1909
1910 #[test]
1911 fn explicit_configuration() {
1912 if std::env::var_os("ADK_EXPLICIT_CONFIG_CHILD").is_none() {
1913 let status = std::process::Command::new(std::env::current_exe().unwrap())
1914 .args(["--exact", "client::tests::explicit_configuration"])
1915 .env("ADK_EXPLICIT_CONFIG_CHILD", "1")
1916 .env("ANTHROPIC_BASE_URL", "invalid-environment-url")
1917 .status()
1918 .unwrap();
1919 assert!(status.success());
1920 return;
1921 }
1922 let key = "sk-explicit-key";
1923 let client = Anthropic::new_with_base_url(key, "http://127.0.0.1:12345").unwrap();
1924 assert_eq!(client.api_key, key);
1925 assert_eq!(client.cached_headers["x-api-key"], key);
1926 assert_eq!(client.base_url, "http://127.0.0.1:12345");
1927 assert!(Anthropic::new_with_base_url(key, "invalid-explicit-url").is_err());
1928
1929 let dir =
1930 std::env::temp_dir().join(format!("adk_anthropic_explicit_{}", std::process::id()));
1931 std::fs::create_dir_all(&dir).unwrap();
1932 let file = dir.join("key.txt");
1933 std::fs::write(&file, "sk-from-file\n").unwrap();
1934 let client =
1935 Anthropic::new_with_base_url(format!("file://{}", file.display()), "https://a.example")
1936 .unwrap();
1937 std::fs::remove_dir_all(&dir).unwrap();
1938 assert_eq!(client.api_key, "sk-from-file");
1939 assert!(
1940 Anthropic::new_with_base_url("file:///adk-missing-key-file", "https://a.example")
1941 .is_err()
1942 );
1943 }
1944
1945 #[test]
1946 fn resolve_api_key_regular_value() {
1947 let result = Anthropic::resolve_api_key("sk-test-key-123");
1948 assert!(result.is_ok());
1949 assert_eq!(result.unwrap(), "sk-test-key-123");
1950 }
1951
1952 #[test]
1953 fn resolve_api_key_file_url_absolute() {
1954 let test_dir =
1955 std::env::temp_dir().join(format!("adk_anthropic_test_{}", std::process::id()));
1956 std::fs::create_dir_all(&test_dir).unwrap();
1957 let test_file = test_dir.join("test_api_key.txt");
1958 std::fs::write(&test_file, "sk-test-from-file-123\n").unwrap();
1959
1960 let file_url = format!("file://{}", test_file.display());
1961 let result = Anthropic::resolve_api_key(&file_url);
1962
1963 std::fs::remove_dir_all(&test_dir).unwrap();
1964
1965 assert!(result.is_ok());
1966 assert_eq!(result.unwrap(), "sk-test-from-file-123");
1967 }
1968
1969 #[test]
1970 fn resolve_api_key_file_url_relative() {
1971 let test_file = "test_relative_key.txt";
1972 std::fs::write(test_file, "sk-relative-key-456\n").unwrap();
1973
1974 let file_url = format!("file://{}", test_file);
1975 let result = Anthropic::resolve_api_key(&file_url);
1976
1977 std::fs::remove_file(test_file).unwrap();
1978
1979 assert!(result.is_ok());
1980 assert_eq!(result.unwrap(), "sk-relative-key-456");
1981 }
1982
1983 #[test]
1984 fn resolve_api_key_file_url_nonexistent() {
1985 let result = Anthropic::resolve_api_key("file:///nonexistent/path/to/key.txt");
1986 assert!(result.is_err());
1987
1988 let error = result.unwrap_err();
1989 assert!(error.is_validation());
1990 assert!(format!("{}", error).contains("Failed to read API key from file"));
1991 }
1992
1993 #[test]
1994 fn resolve_api_key_file_url_with_whitespace() {
1995 let test_file = "test_whitespace_key.txt";
1996 std::fs::write(test_file, " sk-whitespace-key-789 \n ").unwrap();
1997
1998 let file_url = format!("file://{}", test_file);
1999 let result = Anthropic::resolve_api_key(&file_url);
2000
2001 std::fs::remove_file(test_file).unwrap();
2002
2003 assert!(result.is_ok());
2004 assert_eq!(result.unwrap(), "sk-whitespace-key-789");
2005 }
2006
2007 #[test]
2008 fn client_builder_methods() {
2009 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
2010
2011 let configured_client = client
2013 .with_base_url("https://custom.api.com".to_string())
2014 .unwrap()
2015 .with_max_retries(5)
2016 .with_backoff_params(2.0, 1.0);
2017
2018 assert_eq!(configured_client.base_url, "https://custom.api.com");
2019 assert_eq!(configured_client.max_retries, 5);
2020 assert_eq!(configured_client.throughput_ops_sec, 2.0);
2021 assert_eq!(configured_client.reserve_capacity, 1.0);
2022 }
2023
2024 #[test]
2025 fn build_url_default_base() {
2026 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
2027 assert_eq!(client.build_url("messages"), "https://api.anthropic.com/v1/messages");
2029 assert_eq!(
2030 client.build_url("messages/count_tokens"),
2031 "https://api.anthropic.com/v1/messages/count_tokens"
2032 );
2033 assert_eq!(client.build_url("models"), "https://api.anthropic.com/v1/models");
2034 }
2035
2036 #[test]
2037 fn build_url_custom_base_without_trailing_slash() {
2038 let client = Anthropic::new(Some("test_key".to_string()))
2039 .unwrap()
2040 .with_base_url("https://api.minimax.io/anthropic".to_string())
2041 .unwrap();
2042 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
2043 }
2044
2045 #[test]
2046 fn build_url_custom_base_with_trailing_slash() {
2047 let client = Anthropic::new(Some("test_key".to_string()))
2048 .unwrap()
2049 .with_base_url("https://api.minimax.io/anthropic/".to_string())
2050 .unwrap();
2051 assert_eq!(client.build_url("messages"), "https://api.minimax.io/anthropic/v1/messages");
2052 }
2053
2054 #[test]
2055 fn build_url_minimax_china() {
2056 let client = Anthropic::new(Some("test_key".to_string()))
2057 .unwrap()
2058 .with_base_url("https://api.minimaxi.com/anthropic".to_string())
2059 .unwrap();
2060 assert_eq!(client.build_url("messages"), "https://api.minimaxi.com/anthropic/v1/messages");
2061 assert_eq!(
2062 client.build_url(&format!("models/{}", "claude-3-opus")),
2063 "https://api.minimaxi.com/anthropic/v1/models/claude-3-opus"
2064 );
2065 }
2066
2067 #[test]
2068 fn with_base_url_accepts_https() {
2069 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
2070 .unwrap()
2071 .with_base_url("https://gateway.example.com/anthropic".to_string())
2072 .unwrap();
2073 assert_eq!(client.base_url, "https://gateway.example.com/anthropic");
2074 }
2075
2076 #[test]
2077 fn with_base_url_allows_loopback_http_for_local_dev() {
2078 for url in ["http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"] {
2079 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
2080 .unwrap()
2081 .with_base_url(url.to_string())
2082 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
2083 assert_eq!(client.base_url, url);
2084 }
2085 }
2086
2087 #[test]
2088 fn with_base_url_rejects_cleartext_http() {
2089 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
2090 .unwrap()
2091 .with_base_url("http://gateway.internal.example.com".to_string())
2092 .expect_err("a non-loopback http base URL must be rejected");
2093
2094 assert!(err.is_validation(), "expected a validation error, got {err}");
2095 let message = err.to_string();
2096 assert!(
2097 message.contains("unencrypted"),
2098 "error should explain the cleartext risk, got: {message}"
2099 );
2100 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
2101 assert!(
2102 message.contains("allow_insecure_http"),
2103 "error should name the opt-in, got: {message}"
2104 );
2105 }
2106
2107 #[test]
2108 fn allow_insecure_http_before_with_base_url_accepts_cleartext_http() {
2109 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
2110 .unwrap()
2111 .allow_insecure_http()
2112 .with_base_url("http://10.60.1.20:8080/api/v1/llm/anthropic".to_string())
2113 .unwrap();
2114 assert_eq!(client.base_url, "http://10.60.1.20:8080/api/v1/llm/anthropic");
2115 assert_eq!(
2116 client.build_url("messages"),
2117 "http://10.60.1.20:8080/api/v1/llm/anthropic/v1/messages"
2118 );
2119
2120 let client = Anthropic::new_with_base_url("placeholder-api-key", DEFAULT_API_URL)
2121 .unwrap()
2122 .allow_insecure_http()
2123 .with_base_url_and_timeout(
2124 "http://gateway.corp.internal".to_string(),
2125 Duration::from_secs(5),
2126 )
2127 .unwrap();
2128 assert_eq!(client.base_url, "http://gateway.corp.internal");
2129 assert_eq!(client.timeout, Duration::from_secs(5));
2130 }
2131
2132 #[test]
2133 fn allow_insecure_http_after_with_base_url_is_too_late() {
2134 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
2135 .unwrap()
2136 .with_base_url("http://10.60.1.20:8080".to_string())
2137 .map(Anthropic::allow_insecure_http)
2138 .expect_err("with_base_url validates before the opt-in is set");
2139 assert!(err.is_validation(), "expected a validation error, got {err}");
2140 assert!(
2141 err.to_string().contains("allow_insecure_http"),
2142 "error should name the opt-in, got: {err}"
2143 );
2144 }
2145
2146 #[test]
2147 fn allow_insecure_http_admits_http_only() {
2148 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
2149 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
2150 .unwrap()
2151 .allow_insecure_http()
2152 .with_base_url(url.to_string())
2153 .expect_err("the opt-in must not admit other schemes");
2154 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
2155 }
2156 }
2157
2158 #[test]
2159 fn new_with_base_url_rejection_names_the_opt_in() {
2160 let err = Anthropic::new_with_base_url("placeholder-api-key", "http://10.60.1.20:8080")
2161 .expect_err("a non-loopback http base URL must be rejected");
2162 assert!(
2163 err.to_string().contains("allow_insecure_http"),
2164 "error should name the opt-in, got: {err}"
2165 );
2166 }
2167
2168 #[test]
2169 fn insecure_http_environment() {
2170 const GATEWAY: &str = "http://10.60.1.20:8080/api/v1/llm/anthropic";
2171 if std::env::var_os("ADK_INSECURE_HTTP_ENV_CHILD").is_none() {
2172 let status = std::process::Command::new(std::env::current_exe().unwrap())
2173 .args(["--exact", "client::tests::insecure_http_environment"])
2174 .env("ADK_INSECURE_HTTP_ENV_CHILD", "1")
2175 .env("ANTHROPIC_BASE_URL", GATEWAY)
2176 .env("ANTHROPIC_ALLOW_INSECURE_HTTP", "true")
2177 .status()
2178 .unwrap();
2179 assert!(status.success());
2180 return;
2181 }
2182 let client = Anthropic::new(Some("placeholder-api-key".to_string()))
2183 .expect("ANTHROPIC_ALLOW_INSECURE_HTTP admits the ANTHROPIC_BASE_URL gateway");
2184 assert_eq!(client.base_url(), GATEWAY);
2185
2186 assert!(!client.allow_insecure_http);
2188 let err = client
2189 .with_base_url("http://gateway.corp.internal".to_string())
2190 .expect_err("a URL given in code needs allow_insecure_http()");
2191 assert!(err.to_string().contains("allow_insecure_http"), "got: {err}");
2192 assert!(Anthropic::new_with_base_url("placeholder-api-key", GATEWAY).is_err());
2193 }
2194
2195 #[test]
2196 fn with_base_url_rejects_non_http_schemes_and_garbage() {
2197 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
2198 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
2199 .unwrap()
2200 .with_base_url(url.to_string())
2201 .expect_err("non-https, non-loopback base URL must be rejected");
2202 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
2203 }
2204 }
2205
2206 #[test]
2213 fn env_base_url_absent_falls_back_to_default() {
2214 let resolved = Anthropic::resolve_base_url(None, None).unwrap();
2215 assert_eq!(resolved, DEFAULT_API_URL);
2216 let resolved = Anthropic::resolve_base_url(None, Some("1".to_string())).unwrap();
2217 assert_eq!(resolved, DEFAULT_API_URL);
2218 }
2219
2220 #[test]
2221 fn env_base_url_accepts_https() {
2222 let resolved = Anthropic::resolve_base_url(
2223 Some("https://gateway.example.com/anthropic".to_string()),
2224 None,
2225 )
2226 .unwrap();
2227 assert_eq!(resolved, "https://gateway.example.com/anthropic");
2228 }
2229
2230 #[test]
2231 fn env_base_url_allows_loopback_http_for_local_dev() {
2232 for url in ["http://localhost:11434", "http://127.0.0.1:11434", "http://[::1]:11434"] {
2233 let resolved = Anthropic::resolve_base_url(Some(url.to_string()), None)
2234 .unwrap_or_else(|e| panic!("loopback url {url} should be accepted: {e}"));
2235 assert_eq!(resolved, url);
2236 }
2237 }
2238
2239 #[test]
2240 fn env_base_url_rejects_cleartext_http() {
2241 for flag in [None, Some("0"), Some("false"), Some("yes"), Some("")] {
2242 let err = Anthropic::resolve_base_url(
2243 Some("http://gateway.internal.example.com".to_string()),
2244 flag.map(str::to_string),
2245 )
2246 .expect_err("a non-loopback http ANTHROPIC_BASE_URL must be rejected");
2247
2248 assert!(err.is_validation(), "expected a validation error, got {err}");
2249 let message = err.to_string();
2250 assert!(
2251 message.contains("unencrypted"),
2252 "error should explain the cleartext risk, got: {message}"
2253 );
2254 assert!(message.contains("'http'"), "error should name the scheme, got: {message}");
2255 assert!(
2256 message.contains("ANTHROPIC_ALLOW_INSECURE_HTTP=1"),
2257 "error should name the environment opt-in for {flag:?}, got: {message}"
2258 );
2259 }
2260 }
2261
2262 #[test]
2263 fn env_base_url_accepts_cleartext_http_when_acknowledged() {
2264 for flag in ["1", "true", "TRUE", " True "] {
2265 let resolved = Anthropic::resolve_base_url(
2266 Some("http://10.60.1.20:8080/api/v1/llm/anthropic".to_string()),
2267 Some(flag.to_string()),
2268 )
2269 .unwrap_or_else(|e| {
2270 panic!("ANTHROPIC_ALLOW_INSECURE_HTTP={flag:?} should admit http: {e}")
2271 });
2272 assert_eq!(resolved, "http://10.60.1.20:8080/api/v1/llm/anthropic");
2273 }
2274 }
2275
2276 #[test]
2277 fn env_base_url_rejects_non_http_schemes_and_garbage() {
2278 for url in ["ftp://files.example.com", "ws://gateway.example.com", "not-a-url"] {
2279 let err = Anthropic::resolve_base_url(Some(url.to_string()), Some("1".to_string()))
2280 .expect_err("non-https, non-loopback ANTHROPIC_BASE_URL must be rejected");
2281 assert!(err.is_validation(), "expected a validation error for {url}, got {err}");
2282 }
2283 }
2284
2285 #[test]
2286 fn with_base_url_and_timeout_rejects_cleartext_http() {
2287 let err = Anthropic::new(Some("placeholder-api-key".to_string()))
2288 .unwrap()
2289 .with_base_url_and_timeout(
2290 "http://gateway.internal.example.com".to_string(),
2291 Duration::from_secs(5),
2292 )
2293 .expect_err("a non-loopback http base URL must be rejected");
2294 assert!(err.is_validation(), "expected a validation error, got {err}");
2295 }
2296
2297 #[test]
2298 fn client_timeout_configuration() {
2299 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
2300 let timeout = Duration::from_secs(30);
2301
2302 let configured_client = client.with_timeout(timeout).unwrap();
2303 assert_eq!(configured_client.timeout, timeout);
2304 }
2305
2306 #[test]
2307 fn client_cached_headers_performance() {
2308 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
2309
2310 let headers1 = client.default_headers();
2312 let headers2 = client.default_headers();
2313
2314 assert_eq!(headers1.len(), headers2.len());
2315 assert!(headers1.contains_key("x-api-key"));
2316 assert!(headers1.contains_key("anthropic-version"));
2317 assert!(headers1.contains_key("content-type"));
2318 }
2319
2320 #[test]
2321 fn request_error_mapping() {
2322 let client = Anthropic::new(Some("test_key".to_string())).unwrap();
2323
2324 let _timeout = Duration::from_secs(30);
2327 assert_eq!(client.timeout, DEFAULT_TIMEOUT); }
2329
2330 #[tokio::test]
2331 async fn concurrent_retry_safety() {
2332 use std::sync::atomic::{AtomicUsize, Ordering};
2333 use tokio::spawn;
2334
2335 let client = Anthropic {
2336 api_key: "test".to_string(),
2337 client: ReqwestClient::new(),
2338 stream_client: ReqwestClient::new(),
2339 base_url: "http://localhost".to_string(),
2340 timeout: Duration::from_secs(1),
2341 stream_timeout: None,
2342 max_retries: 1,
2343 throughput_ops_sec: 1.0,
2344 reserve_capacity: 1.0,
2345 cached_headers: Arc::new(HeaderMap::new()),
2346 allow_insecure_http: false,
2347 };
2348
2349 let attempt_counter = Arc::new(AtomicUsize::new(0));
2350 let mut handles = vec![];
2351
2352 for _ in 0..3 {
2354 let client_clone = client.clone();
2355 let counter_clone = attempt_counter.clone();
2356
2357 let handle = spawn(async move {
2358 client_clone
2359 .retry_with_backoff(|| {
2360 let counter = counter_clone.clone();
2361 async move {
2362 counter.fetch_add(1, Ordering::SeqCst);
2363 Ok::<String, Error>("success".to_string())
2364 }
2365 })
2366 .await
2367 });
2368 handles.push(handle);
2369 }
2370
2371 for handle in handles {
2373 let result = handle.await.unwrap();
2374 assert!(result.is_ok());
2375 }
2376
2377 assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
2379 }
2380}