Skip to main content

adk_anthropic/
client.rs

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
33/// Simple base64 encoding for skill content.
34fn base64_encode(data: &[u8]) -> String {
35    base64::engine::general_purpose::STANDARD.encode(data)
36}
37
38/// A stream wrapper that logs events and the final message through a [`ClientLogger`].
39///
40/// This stream passes through all events from the underlying [`AccumulatingStream`],
41/// logging each event as it occurs and logging the final reconstructed message
42/// when the stream completes.
43pub 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    /// Create a new logging stream wrapper.
51    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                // Stream ended - try to get the accumulated message
73                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/// Client for the Anthropic API with performance optimizations.
92#[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 for performance - Arc for cheap cloning
102    cached_headers: Arc<HeaderMap>,
103}
104
105impl Anthropic {
106    /// Resolve an API key value, handling file:// URLs
107    fn resolve_api_key(key_value: &str) -> Result<String> {
108        if let Some(stripped) = key_value.strip_prefix("file://") {
109            // Handle file:// URLs
110            let path = if stripped.starts_with('/') {
111                // Absolute path: file:///root/.env -> /root/.env
112                stripped.to_string()
113            } else {
114                // Relative path: file://../foo -> ../foo
115                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            // Regular API key value
126            Ok(key_value.to_string())
127        }
128    }
129
130    /// Resolve the effective base URL from an optional `ANTHROPIC_BASE_URL` value.
131    ///
132    /// A value supplied through the environment is held to exactly the same rule
133    /// as one supplied through [`Anthropic::with_base_url`]: every request
134    /// attaches the API key, so an unencrypted endpoint would leak the
135    /// credential. When no value is supplied the default Anthropic API URL is
136    /// used.
137    ///
138    /// # Errors
139    ///
140    /// Returns a validation error when the supplied value is not `https://` and
141    /// not `http://` with a loopback host.
142    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    /// Create a new Anthropic client.
153    ///
154    /// The API key can be provided directly or read from the `ANTHROPIC_API_KEY`
155    /// environment variable. If the value starts with `"file://"`, it will be
156    /// treated as a file path and the API key will be read from that file.
157    ///
158    /// The base URL is resolved from the `ANTHROPIC_BASE_URL` environment
159    /// variable. If not set, the default Anthropic API URL is used.
160    ///
161    /// # Errors
162    ///
163    /// Returns a validation error when `ANTHROPIC_BASE_URL` is set to an
164    /// endpoint that would transmit the API key in cleartext — anything other
165    /// than `https://`, or `http://` with a loopback host (`localhost`,
166    /// `127.0.0.1`, `[::1]`). A misconfigured environment fails loudly rather
167    /// than silently falling back to the default URL.
168    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) // Connection pooling optimization
185            .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        // Pre-build headers for performance
193        let cached_headers = Arc::new(Self::build_default_headers(&api_key)?);
194
195        // Resolve base URL from environment variable, defaulting to the API URL.
196        // An env-provided value is validated here so the cleartext path closed on
197        // `with_base_url` cannot be reopened through the environment.
198        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    /// Create an Anthropic client authenticated with a bearer token.
213    ///
214    /// Unlike [`Anthropic::new`], this constructor does not require an API key or
215    /// the `ANTHROPIC_API_KEY` environment variable. Requests carry
216    /// `Authorization: Bearer <token>` and omit `x-api-key`.
217    ///
218    /// # Errors
219    ///
220    /// Returns a validation error when the token is empty or cannot be encoded
221    /// as an HTTP header value.
222    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    /// Replace API-key authentication with a bearer token.
227    ///
228    /// The resulting client omits `x-api-key` from every request.
229    ///
230    /// # Errors
231    ///
232    /// Returns a validation error when the token is empty or cannot be encoded
233    /// as an HTTP header value.
234    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    /// Override the `anthropic-version` header used by this client.
258    ///
259    /// # Errors
260    ///
261    /// Returns a validation error when the version is empty or cannot be
262    /// encoded as an HTTP header value.
263    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    /// Set a custom base URL for this client.
285    ///
286    /// This method allows you to specify a different API endpoint for the client.
287    /// The base URL should be the root URL without the `/v1/` suffix - this will
288    /// be added automatically when constructing request URLs.
289    ///
290    /// # Errors
291    ///
292    /// Every request made by this client attaches the Anthropic API key, so the
293    /// base URL must be encrypted. Returns a validation error unless the URL uses
294    /// `https://`, or `http://` with a loopback host (`localhost`, `127.0.0.1`,
295    /// `[::1]`) for local development.
296    ///
297    /// # Examples
298    ///
299    /// ```
300    /// # use adk_anthropic::Anthropic;
301    /// // For Anthropic's API (default)
302    /// let client = Anthropic::new(Some("placeholder-api-key".to_string()))?
303    ///     .with_base_url("https://api.anthropic.com".to_string())?;
304    ///
305    /// // For Minimax (international)
306    /// let client = Anthropic::new(Some("placeholder-api-key".to_string()))?
307    ///     .with_base_url("https://api.minimax.io/anthropic".to_string())?;
308    ///
309    /// // For Minimax (China)
310    /// let client = Anthropic::new(Some("placeholder-api-key".to_string()))?
311    ///     .with_base_url("https://api.minimaxi.com/anthropic".to_string())?;
312    /// # Ok::<(), adk_anthropic::Error>(())
313    /// ```
314    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    /// Return the effective API base URL used for message requests.
321    pub fn base_url(&self) -> &str {
322        &self.base_url
323    }
324
325    /// Set a custom timeout for this client.
326    ///
327    /// This method allows you to specify a different timeout for API requests.
328    pub fn with_timeout(mut self, timeout: Duration) -> Result<Self> {
329        self.timeout = timeout;
330
331        // Recreate the client with the new timeout and performance optimizations
332        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    /// Set the maximum number of retries for this client.
350    ///
351    /// This method allows you to specify how many times to retry failed requests.
352    pub fn with_max_retries(mut self, max_retries: usize) -> Self {
353        self.max_retries = max_retries;
354        self
355    }
356
357    /// Get the API key being used by this client.
358    pub fn api_key(&self) -> &str {
359        &self.api_key
360    }
361
362    /// Set the backoff parameters for this client.
363    ///
364    /// This method allows you to configure the exponential backoff algorithm.
365    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    /// Set both a custom base URL and timeout for this client.
372    ///
373    /// This is a convenience method that chains with_base_url and with_timeout.
374    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    /// Build default headers for API requests (static method for initialization).
379    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    /// Get cached headers for performance (no allocation needed).
397    fn default_headers(&self) -> HeaderMap {
398        (*self.cached_headers).clone()
399    }
400
401    /// Return a copy of the headers this client normally sends.
402    ///
403    /// Callers can modify this map and pass it to
404    /// [`Anthropic::send_with_headers`] or [`Anthropic::stream_with_headers`].
405    /// Those methods use replacement semantics, so the supplied map is sent
406    /// instead of these defaults.
407    pub fn default_headers_for_request(&self) -> HeaderMap {
408        self.default_headers()
409    }
410
411    /// Build a full endpoint URL from the base URL and endpoint path.
412    ///
413    /// This method handles trailing slashes gracefully and always inserts `/v1/`
414    /// between the base URL and endpoint path. This allows the base URL to be
415    /// specified without requiring a specific format (with or without trailing slash,
416    /// with or without `/v1/` suffix).
417    ///
418    /// # Examples
419    ///
420    /// - Base: `https://api.anthropic.com`, endpoint: `messages` → `https://api.anthropic.com/v1/messages`
421    /// - Base: `https://api.minimax.io/anthropic`, endpoint: `messages` → `https://api.minimax.io/anthropic/v1/messages`
422    /// - Base: `https://example.com/`, endpoint: `models` → `https://example.com/v1/models`
423    fn build_url(&self, endpoint: &str) -> String {
424        let base = self.base_url.trim_end_matches('/');
425        format!("{}/v1/{}", base, endpoint)
426    }
427
428    /// Retry wrapper that implements exponential backoff with header-based retry-after
429    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                    // Check if error is retryable
442                    if !error.is_retryable() {
443                        return Err(error);
444                    }
445
446                    // Don't sleep on the last attempt
447                    if attempt == self.max_retries {
448                        last_error = Some(error);
449                        break;
450                    }
451
452                    // Calculate backoff duration
453                    let exp_backoff_duration = backoff.next();
454
455                    // Get retry-after from error if available
456                    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                    // Take the maximum of exponential backoff and header-based backoff
467                    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    /// Process API response errors and convert to our Error type
485    async fn process_error_response(response: Response) -> Error {
486        let status = response.status();
487        let status_code = status.as_u16();
488
489        // Get headers we might need for error processing
490        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        // Try to parse error response body
503        #[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        // Try to parse as JSON first
527        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        // Map HTTP status code to appropriate error type
539        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    /// Convert reqwest errors to appropriate Error types
554    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    /// Execute a POST request with error handling
565    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    /// Execute a GET request with error handling
592    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        // Validate parameters first
684        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        // Ensure stream is disabled
691        params.stream = false;
692
693        // Task 8.1: When thinking is Enabled, force temperature to 1.0
694        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(&params, &[], 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, &params, 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    /// Send a message to the API and get a non-streaming response.
718    pub async fn send(&self, params: MessageCreateParams) -> Result<Message> {
719        self.send_with_resolved_headers(params, None).await
720    }
721
722    /// Send a message using an exact replacement header map.
723    ///
724    /// The supplied headers replace all client defaults and automatically
725    /// generated beta headers. This permits per-request bearer authentication,
726    /// API versions, beta selection, and deliberate beta suppression. Start
727    /// from [`Anthropic::default_headers_for_request`] when only a small change
728    /// is needed.
729    ///
730    /// # Errors
731    ///
732    /// Returns an error when the parameters are invalid, a header is invalid,
733    /// or the request fails.
734    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    /// Send a message with caller-selected Anthropic beta versions.
743    ///
744    /// Caller-selected betas are de-duplicated with beta headers required by
745    /// the request's typed features. They are sent only as headers and never
746    /// serialized into the JSON body.
747    ///
748    /// # Errors
749    ///
750    /// Returns an error when a beta identifier or message parameter is invalid,
751    /// or the request fails.
752    pub async fn send_with_betas(
753        &self,
754        params: MessageCreateParams,
755        betas: &[&str],
756    ) -> Result<Message> {
757        let headers = self.message_headers(&params, betas, false)?;
758        self.send_with_resolved_headers(params, Some(headers)).await
759    }
760
761    /// Send a message to the API with logging and get a non-streaming response.
762    ///
763    /// This method is identical to [`send`](Self::send) but additionally logs
764    /// the response through the provided [`ClientLogger`].
765    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    /// Send a message to the API and get a streaming response.
778    ///
779    /// Returns a stream of MessageStreamEvent objects that can be processed incrementally.
780    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        // Validate parameters first
789        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        // Task 8.3: Clone and force stream = true in the request body
796        let mut params = params.clone();
797        params.stream = true;
798
799        // Task 8.1: When thinking is Enabled, force temperature to 1.0
800        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(&params, &[], 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(&params)
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        // Get the byte stream from the response
840        let stream = response.bytes_stream();
841
842        // Create an SSE processor
843        Ok(process_sse(stream))
844    }
845
846    /// Send a message to the API and stream response events.
847    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    /// Stream a message using an exact replacement header map.
855    ///
856    /// The supplied headers replace all defaults, including `Accept`,
857    /// authentication, API-version, and generated beta headers. Callers should
858    /// normally include `Accept: text/event-stream`.
859    ///
860    /// # Errors
861    ///
862    /// Returns an error when the parameters are invalid, a header is invalid,
863    /// or the request fails.
864    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    /// Stream a message with caller-selected Anthropic beta versions.
873    ///
874    /// Caller-selected betas are de-duplicated with beta headers required by
875    /// the request's typed features. They are sent only as headers and never
876    /// serialized into the JSON body.
877    ///
878    /// # Errors
879    ///
880    /// Returns an error when a beta identifier or message parameter is invalid,
881    /// or the request fails.
882    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    /// Send a message with Claude server-side refusal fallback enabled.
892    ///
893    /// The beta API retries only safety-classifier refusals. Rate limits,
894    /// overloads, and server failures are returned without fallback.
895    ///
896    /// # Errors
897    ///
898    /// Returns an error when the request or fallback configuration is invalid,
899    /// or the request fails.
900    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    /// Stream a message with Claude server-side refusal fallback enabled.
932    ///
933    /// # Errors
934    ///
935    /// Returns an error when the request or fallback configuration is invalid,
936    /// or the request fails.
937    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    /// Send a message to the API with logging and get a streaming response.
985    ///
986    /// This method is identical to [`stream`](Self::stream) but additionally logs
987    /// each streaming event and the final reconstructed message through the
988    /// provided [`ClientLogger`].
989    ///
990    /// Returns a [`LoggingStream`] that wraps an [`AccumulatingStream`], logging
991    /// each event as it passes through and logging the final message when the
992    /// stream completes.
993    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    /// Count tokens for a message.
1004    ///
1005    /// This method counts the number of tokens that would be used by a message with the given parameters.
1006    /// It's useful for estimating costs or making sure your messages fit within the model's context window.
1007    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, &params, 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    /// List available models from the API.
1028    ///
1029    /// Returns a paginated list of all available models. Use the parameters to control
1030    /// pagination and filter results.
1031    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    /// Retrieve information about a specific model.
1064    ///
1065    /// Returns detailed information about the specified model, including its
1066    /// ID, creation date, display name, and type.
1067    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    // --- Helper methods for DELETE and multipart requests ---
1085
1086    /// Execute a DELETE request with error handling.
1087    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    /// Build standard pagination query params.
1104    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    // --- Batches API (Req 13) ---
1123
1124    /// Create a message batch for asynchronous processing.
1125    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    /// Get the status of a message batch.
1143    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    /// Get results of a completed batch as newline-delimited JSON.
1160    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    /// Cancel an in-progress batch.
1205    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    /// Delete a batch.
1222    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    /// List message batches with pagination.
1239    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    // --- Files API (Req 21) ---
1262
1263    /// Upload a file via multipart form upload.
1264    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    /// Get metadata for a file.
1328    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    /// Delete a file.
1345    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    /// List files with pagination.
1362    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    // --- Skills API (Req 22) ---
1385
1386    /// Create a new skill.
1387    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    /// Get a skill by ID.
1414    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    /// Update a skill's content.
1431    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    /// Delete a skill.
1451    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    /// List skills with pagination.
1468    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        // Should only attempt once since authentication errors are not retryable
1560        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        // Should attempt max_retries + 1 times (3 total: initial + 2 retries)
1592        assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1593    }
1594
1595    #[tokio::test]
1596    async fn error_529_is_retryable() {
1597        // Test that 529 errors are properly mapped to rate_limit and are retryable
1598        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                            // Simulate a 529 overloaded error
1620                            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        // Should retry: initial attempt + 2 retries = 3 total
1636        assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1637    }
1638
1639    #[test]
1640    fn error_529_mapped_correctly() {
1641        // Test that a 529 API error is correctly identified as retryable
1642        let error =
1643            Error::api(529, Some("overloaded_error".to_string()), "Overloaded".to_string(), None);
1644        assert!(error.is_retryable());
1645
1646        // Test that rate_limit error (which 529 now maps to) is also retryable
1647        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        // Test builder pattern methods
1718        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        // Default base URL: https://api.anthropic.com
1734        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    // The `ANTHROPIC_BASE_URL` tests exercise `resolve_base_url` directly instead
1821    // of mutating the process environment. The environment is process-global, so
1822    // setting an intentionally-rejected value would race against every other test
1823    // in this binary that calls `Anthropic::new`. `resolve_base_url` is the exact
1824    // code path `Anthropic::new` uses for the env value, so nothing is lost.
1825
1826    #[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        // Test that headers are cached and cloning is cheap
1899        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        // Test different types of reqwest errors are mapped correctly
1913        // Note: These are unit tests for the mapping logic, not integration tests
1914        let _timeout = Duration::from_secs(30);
1915        assert_eq!(client.timeout, DEFAULT_TIMEOUT); // Should use default initially
1916    }
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        // Spawn multiple concurrent retry operations
1938        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        // Wait for all operations to complete
1957        for handle in handles {
1958            let result = handle.await.unwrap();
1959            assert!(result.is_ok());
1960        }
1961
1962        // Verify all operations executed
1963        assert_eq!(attempt_counter.load(Ordering::SeqCst), 3);
1964    }
1965}