#![doc = include_str!("../README.md")]
pub use im;
pub mod anthropic;
pub mod conversation;
pub mod http_request;
use std::sync::Arc;
use crate::{anthropic::ApiResponse, http_request::HttpRequest};
#[derive(Clone, Debug)]
pub struct Api {
api_key: Arc<str>,
default_model: Arc<str>,
default_max_tokens: u32,
endpoint_host: Arc<str>,
}
impl Api {
pub fn new<S: Into<Arc<str>>>(api_key: S) -> Self {
Self {
api_key: api_key.into(),
default_model: Arc::from(anthropic::DEFAULT_MODEL),
default_max_tokens: 1024,
endpoint_host: Arc::from(anthropic::DEFAULT_ENDPOINT_HOST),
}
}
pub fn default_model<S: Into<Arc<str>>>(mut self, model: S) -> Self {
self.default_model = model.into();
self
}
pub fn default_max_tokens(mut self, max_tokens: u32) -> Self {
self.default_max_tokens = max_tokens;
self
}
pub fn endpoint_host<S: Into<Arc<str>>>(mut self, endpoint_host: S) -> Self {
self.endpoint_host = endpoint_host.into();
self
}
fn create_default_headers(&self) -> Vec<(&'static str, Arc<str>)> {
vec![
("content-type", Arc::from("application/json")),
("anthropic-version", Arc::from(anthropic::ANTHROPIC_VERSION)),
("x-api-key", self.api_key.clone()),
]
}
}
#[derive(Debug)]
pub struct MessagesRequestBuilder {
model: Option<String>,
max_tokens: Option<u32>,
system: Option<Arc<str>>,
messages: im::Vector<anthropic::Message>,
tools: Option<im::Vector<anthropic::Tool>>,
stream: bool,
}
impl Default for MessagesRequestBuilder {
fn default() -> Self {
Self::new()
}
}
impl MessagesRequestBuilder {
pub fn new() -> Self {
Self {
model: None,
max_tokens: None,
system: None,
messages: im::Vector::new(),
tools: None,
stream: false,
}
}
pub fn model<S: Into<String>>(mut self, model: S) -> Self {
self.model = Some(model.into());
self
}
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn system<S: Into<Arc<str>>>(mut self, system: S) -> Self {
self.system = Some(system.into());
self
}
pub fn push(mut self, message: anthropic::Message) -> Self {
self.messages.push_back(message);
self
}
pub fn push_message<S: Into<String>>(self, role: anthropic::Role, text: S) -> Self {
let message = anthropic::Message::from_text(role, text);
self.push(message)
}
pub fn set_messages(mut self, messages: im::Vector<anthropic::Message>) -> Self {
self.messages = messages;
self
}
pub fn set_tools<T: Into<im::Vector<anthropic::Tool>>>(mut self, tools: T) -> Self {
self.tools = Some(tools.into());
self
}
pub fn stream(mut self, stream: bool) -> Self {
self.stream = stream;
self
}
pub fn build(&self, api: &Api) -> HttpRequest {
let mut headers = api.create_default_headers();
if let Some(model) = &self.model {
headers.push(("anthropic-model", Arc::from(model.as_str())));
} else {
headers.push(("anthropic-model", api.default_model.clone()));
}
if let Some(max_tokens) = self.max_tokens {
headers.push(("max-tokens", Arc::from(max_tokens.to_string())));
} else {
headers.push(("max-tokens", Arc::from(api.default_max_tokens.to_string())));
}
let body = {
let model = if let Some(ref model) = self.model {
model.as_str()
} else {
&api.default_model
};
let system = self.system.as_deref();
let body = anthropic::MessagesBody {
model,
max_tokens: self.max_tokens.unwrap_or(api.default_max_tokens),
system,
messages: &self.messages,
tools: self.tools.as_ref(),
stream: self.stream,
};
serde_json::to_string(&body).expect("failed to serialize messages")
};
HttpRequest {
host: api.endpoint_host.to_string(),
path: "/v1/messages".to_string(),
method: "POST",
headers,
body,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ResponseError {
#[error("Deserialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("API error: {0}")]
Api(#[from] anthropic::ApiError),
#[error("Unexpected response type")]
UnexpectedResponseType {
expected: &'static str,
actual: &'static str,
},
}
pub fn deserialize_response<T>(json: &str) -> Result<T, ResponseError>
where
T: TryFrom<ApiResponse, Error = ()>,
{
let api_response: ApiResponse = serde_json::from_str(json)?;
match api_response {
ApiResponse::Error { error } => Err(ResponseError::Api(error)),
other => {
let kind = other.kind();
other
.try_into()
.map_err(|()| ResponseError::UnexpectedResponseType {
expected: std::any::type_name::<T>(),
actual: kind,
})
}
}
}
pub fn deserialize_event(data: &[u8]) -> Result<anthropic::StreamEvent, serde_json::Error> {
match serde_json::from_slice(data) {
Ok(event) => Ok(event),
Err(original_error) => {
let contents: serde_json::Value = serde_json::from_slice(data)?;
if let Some(event_type) = contents.get("type").and_then(|v| v.as_str()) {
Ok(anthropic::StreamEvent::Unknown {
event_type: event_type.as_bytes().to_vec(),
contents,
})
} else {
Err(original_error)
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
anthropic::{ApiError, Content, MessagesResponse, Role, StopReason},
deserialize_response,
};
#[test]
fn test_api_response_error_deserialization() {
let json = r#"{
"type": "error",
"error": {
"type": "not_found_error",
"message": "The requested resource could not be found."
}
}"#;
let result: Result<MessagesResponse, _> = deserialize_response(json);
assert!(result.is_err());
if let Err(super::ResponseError::Api(api_error)) = result {
assert!(matches!(api_error, ApiError::NotFoundError));
} else {
panic!("Expected Api error");
}
}
#[test]
fn test_api_response_invalid_request_deserialization() {
let json = r#"{
"error": {
"message": "Invalid request",
"type": "invalid_request_error"
},
"type": "error"
}"#;
let result: Result<MessagesResponse, _> = deserialize_response(json);
assert!(result.is_err());
if let Err(super::ResponseError::Api(api_error)) = result {
assert!(matches!(api_error, ApiError::InvalidRequestError));
} else {
panic!("Expected Api error");
}
}
#[test]
fn test_api_response_message_deserialization() {
let json = r#"{
"content": [
{
"text": "Hi! My name is Claude.",
"type": "text"
}
],
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
"model": "claude-3-7-sonnet-20250219",
"role": "assistant",
"stop_reason": "end_turn",
"stop_sequence": null,
"type": "message",
"usage": {
"input_tokens": 2095,
"output_tokens": 503
}
}"#;
let response: MessagesResponse =
deserialize_response(json).expect("should deserialize API message response");
assert_eq!(response.id, "msg_013Zva2CMHLNnXjNJJKqJ2EF");
assert_eq!(response.model, "claude-3-7-sonnet-20250219");
assert!(matches!(response.message.role, Role::Assistant));
assert_eq!(response.stop_reason, StopReason::EndTurn);
assert_eq!(response.stop_sequence, None);
assert_eq!(response.usage.input_tokens, 2095);
assert_eq!(response.usage.output_tokens, 503);
assert_eq!(response.message.content.len(), 1);
let Content::Text { text } = &response.message.content[0] else {
panic!("should be text");
};
assert_eq!(text, "Hi! My name is Claude.");
}
#[test]
fn test_messages_request_builder_with_system_prompt() {
let api = super::Api::new("test-api-key");
let http_request = super::MessagesRequestBuilder::new()
.system("You are a helpful assistant.")
.push_message(super::anthropic::Role::User, "Hello!")
.build(&api);
assert_eq!(http_request.method, "POST");
assert_eq!(http_request.path, "/v1/messages");
assert_eq!(http_request.host, "api.anthropic.com");
assert!(
http_request
.body
.contains("\"system\":\"You are a helpful assistant.\"")
);
assert!(http_request.body.contains("\"messages\":["));
assert!(http_request.body.contains("\"role\":\"user\""));
assert!(http_request.body.contains("\"text\":\"Hello!\""));
}
#[test]
fn test_messages_request_builder_with_tools() {
use schemars::JsonSchema;
#[derive(JsonSchema)]
#[allow(dead_code)]
struct WeatherInput {
location: String,
unit: Option<String>,
}
let api = super::Api::new("test-api-key");
let weather_tool = super::anthropic::Tool::new::<WeatherInput, _, _>(
"get_weather",
"Get the current weather in a given location",
);
let tools = im::vector![weather_tool];
let http_request = super::MessagesRequestBuilder::new()
.push_message(
super::anthropic::Role::User,
"What's the weather in San Francisco?",
)
.set_tools(tools)
.build(&api);
assert_eq!(http_request.method, "POST");
assert_eq!(http_request.path, "/v1/messages");
assert_eq!(http_request.host, "api.anthropic.com");
assert!(http_request.body.contains("\"tools\":["));
assert!(http_request.body.contains("\"name\":\"get_weather\""));
assert!(
http_request
.body
.contains("\"description\":\"Get the current weather in a given location\"")
);
assert!(http_request.body.contains("\"input_schema\""));
assert!(http_request.body.contains("\"properties\""));
assert!(http_request.body.contains("\"location\""));
assert!(http_request.body.contains("\"unit\""));
assert!(http_request.body.contains("\"required\":[\"location\"]"));
assert!(http_request.body.contains("\"messages\":["));
assert!(http_request.body.contains("\"role\":\"user\""));
assert!(
http_request
.body
.contains("\"What's the weather in San Francisco?\"")
);
}
}