anthropic_api/
lib.rs

1//! # Anthropic Rust SDK
2//!
3//! An unofficial Rust library for interacting with the [Anthropic API](https://www.anthropic.com/api).
4//! This library provides an asynchronous interface to Anthropic's services, allowing Rust developers
5//! to seamlessly integrate Anthropic's AI capabilities into their applications.
6//!
7//! ## Features
8//!
9//! - **Asynchronous API Requests**: Leverage Rust's async capabilities for efficient API interactions
10//! - **Message API**: Send and receive messages, similar to chat-based interactions
11//! - **Tool Use**: Integrate external tools that the AI can call during responses
12//! - **Streaming Responses**: Receive real-time streamed responses from the API
13//!
14//! ## Basic Usage
15//!
16//! ```no_run
17//! use anthropic_api::{messages::*, Credentials};
18//!
19//! #[tokio::main]
20//! async fn main() {
21//!     // Load credentials from the environment
22//!     let credentials = Credentials::from_env();
23//!
24//!     // Create a message
25//!     let messages = vec![Message {
26//!         role: MessageRole::User,
27//!         content: MessageContent::Text("Hello, Claude!".to_string()),
28//!     }];
29//!
30//!     // Send the message to the Anthropic API
31//!     let response = MessagesAPI::builder("claude-3-7-sonnet-20250219", messages.clone(), 1024)
32//!         .credentials(credentials.clone())
33//!         .create()
34//!         .await
35//!         .unwrap();
36//!
37//!     // Print the assistant's response
38//!     if let Some(ResponseContentBlock::Text { text }) = response.content.first() {
39//!         println!("Assistant: {}", text.trim());
40//!     }
41//! }
42//! ```
43
44use reqwest::{header::CONTENT_TYPE, Client, Method, RequestBuilder, Response};
45use reqwest_eventsource::{CannotCloneRequestError, EventSource, RequestBuilderExt};
46use serde::{de::DeserializeOwned, Deserialize, Serialize};
47use std::env;
48use std::env::VarError;
49use std::sync::{LazyLock, RwLock};
50
51pub mod admin;
52pub mod messages;
53pub mod models;
54/// Default base URL for the Anthropic API
55pub static DEFAULT_BASE_URL: LazyLock<String> =
56    LazyLock::new(|| String::from("https://api.anthropic.com/v1/"));
57/// Default credentials loaded from environment variables
58static DEFAULT_CREDENTIALS: LazyLock<RwLock<Credentials>> =
59    LazyLock::new(|| RwLock::new(Credentials::from_env()));
60
61/// Holds the API key and base URL for an Anthropic-compatible API.
62///
63/// This struct is used to authenticate requests to the Anthropic API.
64/// It can be created from environment variables or explicitly with an API key and base URL.
65#[derive(Debug, Clone, Eq, PartialEq)]
66pub struct Credentials {
67    api_key: String,
68    base_url: String,
69}
70
71impl Credentials {
72    /// Creates credentials with the given API key and base URL.
73    ///
74    /// If the base URL is empty, it will use the default Anthropic API URL.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use anthropic_api::Credentials;
80    ///
81    /// let credentials = Credentials::new("your-api-key", "");
82    /// ```
83    pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
84        let base_url = base_url.into();
85        let base_url = if base_url.is_empty() {
86            DEFAULT_BASE_URL.clone()
87        } else {
88            parse_base_url(base_url)
89        };
90        Self {
91            api_key: api_key.into(),
92            base_url,
93        }
94    }
95
96    /// Fetches the credentials from the environment variables
97    /// `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`.
98    ///
99    /// # Panics
100    ///
101    /// This function will panic if the `ANTHROPIC_API_KEY` variable is missing from the environment.
102    /// If only the `ANTHROPIC_BASE_URL` variable is missing, it will use the default URL.
103    ///
104    /// # Examples
105    ///
106    /// ```no_run
107    /// use anthropic_api::Credentials;
108    ///
109    /// // Assumes ANTHROPIC_API_KEY is set in the environment
110    /// let credentials = Credentials::from_env();
111    /// ```
112    pub fn from_env() -> Credentials {
113        let api_key = env::var("ANTHROPIC_API_KEY").unwrap();
114        let base_url_unparsed = env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|e| match e {
115            VarError::NotPresent => DEFAULT_BASE_URL.clone(),
116            VarError::NotUnicode(v) => panic!("ANTHROPIC_BASE_URL is not unicode: {v:#?}"),
117        });
118        let base_url = parse_base_url(base_url_unparsed);
119        Credentials { api_key, base_url }
120    }
121
122    /// Returns the API key
123    pub fn api_key(&self) -> &str {
124        &self.api_key
125    }
126
127    /// Returns the base URL
128    pub fn base_url(&self) -> &str {
129        &self.base_url
130    }
131}
132
133/// Represents an error returned by the Anthropic API
134#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
135pub struct AnthropicError {
136    /// The type of error
137    #[serde(rename = "type")]
138    pub error_type: String,
139    /// The error message
140    pub message: String,
141}
142
143/// Represents an error response from the Anthropic API
144#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
145pub struct AnthropicErrorResponse {
146    /// The type of response (always "error")
147    #[serde(rename = "type")]
148    pub response_type: String,
149    /// The error details
150    pub error: AnthropicError,
151}
152
153impl AnthropicErrorResponse {
154    /// Creates a new error response with the given message and error type
155    fn new(message: String, error_type: String) -> AnthropicErrorResponse {
156        AnthropicErrorResponse {
157            response_type: "error".to_string(),
158            error: AnthropicError {
159                message,
160                error_type,
161            },
162        }
163    }
164}
165
166impl std::fmt::Display for AnthropicErrorResponse {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.write_str(&self.error.message)
169    }
170}
171
172impl std::error::Error for AnthropicErrorResponse {}
173
174/// Represents a response from the Anthropic API, which can be either a success or an error
175#[derive(Deserialize, Clone)]
176#[serde(untagged)]
177pub enum ApiResponse<T> {
178    /// An error response
179    Err { error: AnthropicErrorResponse },
180    /// A successful response
181    Ok(T),
182}
183
184/// Represents token usage statistics for a request and response
185#[derive(Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
186pub struct Usage {
187    /// Number of tokens in the input
188    pub input_tokens: u32,
189    /// Number of tokens in the output
190    pub output_tokens: u32,
191    /// Number of tokens used for cache creation, if applicable
192    pub cache_creation_input_tokens: Option<u32>,
193    /// Number of tokens read from cache, if applicable
194    pub cache_read_input_tokens: Option<u32>,
195}
196
197/// Result type for Anthropic API responses
198pub type ApiResponseOrError<T> = Result<T, AnthropicErrorResponse>;
199
200impl From<reqwest::Error> for AnthropicErrorResponse {
201    fn from(value: reqwest::Error) -> Self {
202        AnthropicErrorResponse::new(value.to_string(), "reqwest".to_string())
203    }
204}
205
206impl From<std::io::Error> for AnthropicErrorResponse {
207    fn from(value: std::io::Error) -> Self {
208        AnthropicErrorResponse::new(value.to_string(), "io".to_string())
209    }
210}
211
212/// Makes a request to the Anthropic API and deserializes the JSON response
213async fn anthropic_request_json<F, T>(
214    method: Method,
215    route: &str,
216    builder: F,
217    credentials_opt: Option<Credentials>,
218) -> ApiResponseOrError<T>
219where
220    F: FnOnce(RequestBuilder) -> RequestBuilder,
221    T: DeserializeOwned,
222{
223    let api_response = anthropic_request(method, route, builder, credentials_opt)
224        .await?
225        .json()
226        .await?;
227
228    match api_response {
229        ApiResponse::Ok(t) => Ok(t),
230        ApiResponse::Err { error } => Err(error),
231    }
232}
233
234/// Makes a request to the Anthropic API
235async fn anthropic_request<F>(
236    method: Method,
237    route: &str,
238    builder: F,
239    credentials_opt: Option<Credentials>,
240) -> ApiResponseOrError<Response>
241where
242    F: FnOnce(RequestBuilder) -> RequestBuilder,
243{
244    let client = Client::new();
245    let credentials =
246        credentials_opt.unwrap_or_else(|| DEFAULT_CREDENTIALS.read().unwrap().clone());
247    let mut request = client.request(method, format!("{}{route}", credentials.base_url));
248
249    request = builder(request);
250
251    let response = request
252        .header("x-api-key", credentials.api_key)
253        .header("anthropic-version", "2023-06-01")
254        .header(CONTENT_TYPE, "application/json")
255        .send()
256        .await?;
257
258    Ok(response)
259}
260
261/// Creates an event source for streaming responses from the Anthropic API
262async fn anthropic_request_stream<F>(
263    method: Method,
264    route: &str,
265    builder: F,
266    credentials_opt: Option<Credentials>,
267) -> Result<EventSource, CannotCloneRequestError>
268where
269    F: FnOnce(RequestBuilder) -> RequestBuilder,
270{
271    let client = Client::new();
272    let credentials =
273        credentials_opt.unwrap_or_else(|| DEFAULT_CREDENTIALS.read().unwrap().clone());
274    let mut request = client.request(method, format!("{}{route}", credentials.base_url));
275    request = builder(request);
276    let stream = request
277        .header("x-api-key", credentials.api_key)
278        .header("anthropic-version", "2023-06-01")
279        .header(CONTENT_TYPE, "application/json")
280        .eventsource()?;
281    Ok(stream)
282}
283
284/// Makes a POST request to the Anthropic API with the given JSON payload
285async fn anthropic_post<J, T>(
286    route: &str,
287    json: &J,
288    credentials_opt: Option<Credentials>,
289) -> ApiResponseOrError<T>
290where
291    J: Serialize + ?Sized,
292    T: DeserializeOwned,
293{
294    anthropic_request_json(
295        Method::POST,
296        route,
297        |request| request.json(json),
298        credentials_opt,
299    )
300    .await
301}
302
303/// Ensures the base URL ends with a trailing slash
304fn parse_base_url(mut value: String) -> String {
305    if !value.ends_with('/') {
306        value += "/";
307    }
308    value
309}
310
311/// Test utilities
312#[cfg(test)]
313pub mod tests {
314    /// Default model to use in tests
315    pub const DEFAULT_LEGACY_MODEL: &str = "claude-3-5-sonnet-20240620";
316}