use std::{env, num::NonZeroU16, sync::Arc};
use eventsource_stream::Eventsource;
use serde::{Deserialize, Serialize};
use crate::{key, response, Key};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)]
pub struct Client {
pub inner: reqwest::Client,
pub key: Arc<Key>,
}
impl Client {
pub const ANTHROPIC_VERSION: &'static str = "2023-06-01";
#[cfg(feature = "prompt-caching")]
pub const BETA: &'static str = "prompt-caching-2024-07-31";
pub const USER_AGENT: &'static str =
concat!(env!("CARGO_PKG_NAME"), "-", env!("CARGO_PKG_VERSION"));
pub const DEFAULT_URL: &'static str =
"https://api.anthropic.com/v1/messages";
pub fn new<K>(key: K) -> std::result::Result<Self, key::InvalidKeyLength>
where
K: TryInto<Key, Error = key::InvalidKeyLength>,
{
Ok(Self::from_key(key.try_into()?))
}
pub fn from_key(key: Key) -> Self {
#[cfg(feature = "log")]
{
log::info!(concat!(
"Creating ",
env!("CARGO_PKG_NAME", " client...")
));
log::debug!(concat!("Crate version: ", env!("CARGO_PKG_VERSION")));
log::debug!("Anthropic version: {}", Self::ANTHROPIC_VERSION);
#[cfg(feature = "beta")]
log::debug!("Anthropic beta: {}", Self::BETA);
}
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
reqwest::header::HeaderValue::from_static("application/json"),
);
headers.insert(
"anthropic-version",
reqwest::header::HeaderValue::from_static(Self::ANTHROPIC_VERSION),
);
#[cfg(feature = "prompt-caching")]
headers.insert(
"anthropic-beta",
reqwest::header::HeaderValue::from_static(Self::BETA),
);
Self {
inner: reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap(),
key: Arc::new(key),
}
}
pub fn request_raw<U>(
&self,
method: reqwest::Method,
url: U,
) -> reqwest::RequestBuilder
where
U: reqwest::IntoUrl,
{
#[cfg(feature = "log")]
{
log::debug!("{} request to {}", method, url.as_str());
}
#[allow(clippy::useless_asref)]
let mut val =
reqwest::header::HeaderValue::from_bytes(self.key.read().as_ref())
.unwrap();
val.set_sensitive(true);
self.inner.request(method, url).header("x-api-key", val)
}
pub async fn get<U>(&self, url: U) -> reqwest::Result<reqwest::Response>
where
U: reqwest::IntoUrl,
{
self.request_raw(reqwest::Method::GET, url).send().await
}
pub async fn post<U, B>(
&self,
url: U,
body: B,
) -> reqwest::Result<reqwest::Response>
where
U: reqwest::IntoUrl,
B: serde::Serialize,
{
let req = self.request_raw(reqwest::Method::POST, url);
#[cfg(feature = "log")]
{
if let Ok(json) = serde_json::to_string_pretty(&body) {
log::debug!("Sending body:\n{}", json);
} else {
log::warn!("Could not serialize body. Request will fail.");
}
}
req.json(&body).send().await
}
pub async fn request<P>(&self, prompt: P) -> Result<crate::Response>
where
P: Serialize,
{
self.request_custom(prompt, Self::DEFAULT_URL).await
}
pub async fn request_custom<P, U>(
&self,
prompt: P,
url: U,
) -> Result<crate::Response>
where
P: Serialize,
U: reqwest::IntoUrl,
{
let json = serde_json::to_value(prompt)?;
let streaming = json["stream"].as_bool().unwrap_or(false);
let response: reqwest::Response = self.post(url, json).await?;
if response.status() != reqwest::StatusCode::OK {
let error: AnthropicErrorWrapper = response.json().await?;
return Err(error.error.into());
}
if streaming {
Ok(crate::Response::Stream {
stream: crate::Stream::new(
response.bytes_stream().eventsource(),
),
})
} else {
let body = response.bytes().await?;
Ok(crate::Response::Message {
message: serde_json::from_slice(&body)?,
})
}
}
pub async fn message<P>(&self, prompt: P) -> Result<response::Message>
where
P: Serialize,
{
let mut json = serde_json::to_value(prompt)?;
json["stream"] = serde_json::Value::Bool(false);
if let crate::Response::Message { message } = self.request(json).await?
{
Ok(message)
} else {
Err(Error::UnexpectedResponse {
message: "Expected a message, got a stream.",
})
}
}
pub async fn stream<P>(&self, prompt: P) -> Result<crate::Stream>
where
P: Serialize,
{
let mut json = serde_json::to_value(prompt)?;
json["stream"] = serde_json::Value::Bool(true);
if let crate::Response::Stream { stream } = self.request(json).await? {
Ok(stream)
} else {
Err(Error::UnexpectedResponse {
message: "Expected a stream, got a message.",
})
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HTTP error: {0}")]
HTTP(#[from] reqwest::Error),
#[error("Parse error: {0}")]
Parse(#[from] serde_json::Error),
#[error("Anthropic error: {0}")]
Anthropic(#[from] AnthropicError),
#[error("Unexpected response: {message}")]
#[allow(missing_docs)]
UnexpectedResponse { message: &'static str },
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
#[allow(missing_docs)]
pub enum AnthropicError {
#[error("invalid request (400): {message}")]
#[serde(rename = "invalid_request_error")]
InvalidRequest { message: String },
#[error("authentication (401): {message}")]
#[serde(rename = "authentication_error")]
Authentication { message: String },
#[error("permission (403): {message}")]
#[serde(rename = "permission_error")]
Permission { message: String },
#[error("not found (404): {message}")]
#[serde(rename = "not_found_error")]
NotFound { message: String },
#[error("request too large (413): {message}")]
RequestTooLarge { message: String },
#[error("rate limit (429): {message}")]
#[serde(rename = "rate_limit_error")]
RateLimit { message: String },
#[error("api error (500): {message}")]
#[serde(rename = "api_error")]
API { message: String },
#[error("overloaded (529): {message}")]
#[serde(rename = "overloaded_error")]
Overloaded { message: String },
#[error("unknown error ({code}): {message}")]
Unknown { code: NonZeroU16, message: String },
}
impl AnthropicError {
pub fn status(&self) -> NonZeroU16 {
match self {
Self::InvalidRequest { .. } => NonZeroU16::new(400).unwrap(),
Self::Authentication { .. } => NonZeroU16::new(401).unwrap(),
Self::Permission { .. } => NonZeroU16::new(403).unwrap(),
Self::NotFound { .. } => NonZeroU16::new(404).unwrap(),
Self::RequestTooLarge { .. } => NonZeroU16::new(413).unwrap(),
Self::RateLimit { .. } => NonZeroU16::new(429).unwrap(),
Self::API { .. } => NonZeroU16::new(500).unwrap(),
Self::Overloaded { .. } => NonZeroU16::new(529).unwrap(),
Self::Unknown { code, .. } => *code,
}
}
}
#[derive(Deserialize)]
#[serde(tag = "error")]
pub(crate) struct AnthropicErrorWrapper {
pub(crate) error: AnthropicError,
}
#[cfg(test)]
mod tests {
use futures::TryStreamExt;
use super::*;
#[test]
fn test_anthropic_error_deserialize() {
const INVALID_REQUEST: &str =
r#"{"type":"invalid_request_error","message":"Invalid request"}"#;
let error: AnthropicError =
serde_json::from_str(INVALID_REQUEST).unwrap();
assert_eq!(
error,
AnthropicError::InvalidRequest {
message: "Invalid request".to_string()
}
);
const AUTHENTICATION: &str = r#"{"type":"authentication_error","message":"Authentication error"}"#;
let error: AnthropicError =
serde_json::from_str(AUTHENTICATION).unwrap();
assert_eq!(
error,
AnthropicError::Authentication {
message: "Authentication error".to_string()
}
);
const PERMISSION: &str =
r#"{"type":"permission_error","message":"Permission denied"}"#;
let error: AnthropicError = serde_json::from_str(PERMISSION).unwrap();
assert_eq!(
error,
AnthropicError::Permission {
message: "Permission denied".to_string()
}
);
const NOT_FOUND: &str =
r#"{"type":"not_found_error","message":"Resource not found"}"#;
let error: AnthropicError = serde_json::from_str(NOT_FOUND).unwrap();
assert_eq!(
error,
AnthropicError::NotFound {
message: "Resource not found".to_string()
}
);
const REQUEST_TOO_LARGE: &str =
r#"{"type":"request_too_large","message":"Request too large"}"#;
let error: AnthropicError =
serde_json::from_str(REQUEST_TOO_LARGE).unwrap();
assert_eq!(
error,
AnthropicError::RequestTooLarge {
message: "Request too large".to_string()
}
);
const RATE_LIMIT: &str =
r#"{"type":"rate_limit_error","message":"Rate limit exceeded"}"#;
let error: AnthropicError = serde_json::from_str(RATE_LIMIT).unwrap();
assert_eq!(
error,
AnthropicError::RateLimit {
message: "Rate limit exceeded".to_string()
}
);
const API: &str =
r#"{"type":"api_error","message":"Internal server error"}"#;
let error: AnthropicError = serde_json::from_str(API).unwrap();
assert_eq!(
error,
AnthropicError::API {
message: "Internal server error".to_string()
}
);
const OVERLOADED: &str =
r#"{"type":"overloaded_error","message":"Service overloaded"}"#;
let error: AnthropicError = serde_json::from_str(OVERLOADED).unwrap();
assert_eq!(
error,
AnthropicError::Overloaded {
message: "Service overloaded".to_string()
}
);
const INVALID_REQUEST_WRAPPED: &str = r#"{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "<string>"
}
}"#;
let error: AnthropicErrorWrapper =
serde_json::from_str(INVALID_REQUEST_WRAPPED).unwrap();
assert_eq!(
error.error,
AnthropicError::InvalidRequest {
message: "<string>".to_string()
}
);
}
use crate::{prompt::message::Role, stream::FilterExt, Prompt};
const CRATE_ROOT: &str = env!("CARGO_MANIFEST_DIR");
const FAKE_API_KEY: &str = "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";
const NO_API_KEY: &str = "API key not found. Create a file named `api.key` in the crate root with your API key.";
fn load_api_key() -> Option<String> {
use std::fs::File;
use std::io::Read;
use std::path::Path;
let mut file =
File::open(Path::new(CRATE_ROOT).join("api.key")).ok()?;
let mut key = String::new();
file.read_to_string(&mut key).unwrap();
Some(key.trim().to_string())
}
#[test]
fn test_client_new() {
let client = Client::new(FAKE_API_KEY.to_string()).unwrap();
assert_eq!(client.key.to_string(), FAKE_API_KEY);
}
#[tokio::test]
#[ignore = "This test requires a real API key."]
async fn test_client_message() {
let key = load_api_key().expect(NO_API_KEY);
let client = Client::new(key).unwrap();
let message = client
.message(Prompt::default().messages([(
Role::User,
"Emit just the \"🙏\" emoji, please.",
)]))
.await
.unwrap();
assert_eq!(message.message.role, Role::Assistant);
assert!(message.to_string().contains("🙏"));
}
#[tokio::test]
#[ignore = "This test requires a real API key."]
async fn test_client_stream() {
let key = load_api_key().expect(NO_API_KEY);
let client = Client::new(key).unwrap();
let stream = client
.stream(Prompt::default().messages([(
Role::User,
"Emit just the \"🙏\" emoji, please.",
)]))
.await
.unwrap();
let msg: String = stream
.filter_rate_limit()
.text()
.try_collect()
.await
.unwrap();
assert_eq!(msg, "🙏");
}
}