use anyhow::{Context, Result};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
pub struct ClaudeApiClient {
client: Client,
api_key: String,
base_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeApiRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeApiResponse {
pub id: String,
pub r#type: String,
pub role: String,
pub content: Vec<ContentBlock>,
pub model: String,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentBlock {
pub r#type: String,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: i32,
pub output_tokens: i32,
}
impl ClaudeApiClient {
pub fn new(api_key: Option<String>) -> Result<Self> {
let api_key = api_key
.or_else(|| env::var("ANTHROPIC_API_KEY").ok())
.context(
"ANTHROPIC_API_KEY not found. Please set it in environment or provide in config.",
)?;
let client = Client::builder()
.timeout(std::time::Duration::from_secs(300)) .build()?;
Ok(Self {
client,
api_key,
base_url: "https://api.anthropic.com".to_string(),
})
}
pub async fn create_completion(
&self,
model: &str,
messages: Vec<Message>,
max_tokens: i32,
temperature: Option<f32>,
system: Option<String>,
) -> Result<ClaudeApiResponse> {
let request = ClaudeApiRequest {
model: model.to_string(),
messages,
max_tokens,
temperature,
system,
};
let response = self
.client
.post(format!("{}/v1/messages", self.base_url))
.header("Content-Type", "application/json")
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.json(&request)
.send()
.await
.context("Failed to send request to Claude API")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"Claude API error ({}): {}",
status,
error_text
));
}
let api_response: ClaudeApiResponse = response
.json()
.await
.context("Failed to parse Claude API response")?;
Ok(api_response)
}
pub async fn simple_completion(
&self,
model: &str,
prompt: &str,
max_tokens: i32,
) -> Result<String> {
let messages = vec![Message {
role: "user".to_string(),
content: prompt.to_string(),
}];
let response = self
.create_completion(
model,
messages,
max_tokens,
Some(0.0), None,
)
.await?;
let text = response
.content
.into_iter()
.map(|block| block.text)
.collect::<Vec<_>>()
.join("");
Ok(text)
}
}