pub mod builder;
pub mod error;
pub mod stream;
pub mod types;
pub use builder::MessageRequestBuilder;
pub use error::ClaudeApiError;
pub use stream::{ContentDelta, MessageAccumulator, SseParser, StreamEvent};
pub use types::*;
use crate::proxy::{LlmProvider, LlmRequest, LlmResponse, LlmRole, LlmUsage};
use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
const API_VERSION: &str = "2023-06-01";
pub struct ClaudeClient {
pub(crate) client: Client,
pub(crate) api_key: String,
pub(crate) base_url: String,
pub(crate) default_model: String,
}
impl ClaudeClient {
pub fn new(api_key: String) -> Self {
Self {
client: Client::new(),
api_key,
base_url: "https://api.anthropic.com/v1".to_string(),
default_model: models::SONNET_4_6.to_string(),
}
}
pub fn with_base_url(mut self, url: String) -> Self {
self.base_url = url;
self
}
pub fn with_default_model(mut self, model: String) -> Self {
self.default_model = model;
self
}
pub fn messages(&self) -> MessageRequestBuilder<'_> {
MessageRequestBuilder::new(self)
}
pub(crate) async fn send_request(
&self,
request: &MessagesRequest,
extra_betas: &[String],
) -> Result<reqwest::Response, ClaudeApiError> {
let url = format!("{}/messages", self.base_url);
let mut req = self
.client
.post(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", API_VERSION)
.header("content-type", "application/json");
if !extra_betas.is_empty() {
req = req.header("anthropic-beta", extra_betas.join(","));
}
let response = req
.json(request)
.send()
.await
.map_err(ClaudeApiError::Network)?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
return Err(ClaudeApiError::from_response(status, &body));
}
Ok(response)
}
}
pub struct AnthropicProvider {
client: ClaudeClient,
}
impl AnthropicProvider {
pub fn new(api_key: String) -> Self {
Self {
client: ClaudeClient::new(api_key),
}
}
pub fn claude_client(&self) -> &ClaudeClient {
&self.client
}
}
impl Default for AnthropicProvider {
fn default() -> Self {
let api_key = std::env::var("ANTHROPIC_API_KEY").unwrap_or_default();
Self::new(api_key)
}
}
#[async_trait]
impl LlmProvider for AnthropicProvider {
async fn complete(&self, request: LlmRequest) -> Result<LlmResponse> {
let mut builder = self
.client
.messages()
.model(&request.model)
.max_tokens(request.max_tokens.unwrap_or(1024));
if let Some(temp) = request.temperature {
builder = builder.temperature(temp);
}
for msg in request.messages {
match msg.role {
LlmRole::System => {
builder = builder.system(&msg.content);
}
LlmRole::User => {
builder = builder.user(&msg.content);
}
LlmRole::Assistant => {
builder = builder.assistant(&msg.content);
}
}
}
let response = builder
.send()
.await
.context("Failed to complete Claude request")?;
let content = response.text().unwrap_or("").to_string();
Ok(LlmResponse {
content,
model: response.model,
usage: Some(LlmUsage::from(response.usage)),
})
}
fn name(&self) -> &'static str {
"Anthropic"
}
}