use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct AnthropicClient {
api_key: String,
client: reqwest::Client,
}
#[derive(Debug, Serialize)]
struct MessagesRequest {
model: String,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
messages: Vec<Message>,
}
#[derive(Debug, Serialize)]
struct Message {
role: String,
content: String,
}
#[derive(Debug, Deserialize)]
struct MessagesResponse {
content: Vec<ContentBlock>,
usage: Usage,
}
#[derive(Debug, Deserialize)]
struct ContentBlock {
text: String,
}
#[derive(Debug, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
}
#[derive(Debug)]
pub struct CompletionResult {
pub text: String,
pub usage: Usage,
}
impl AnthropicClient {
pub fn new(api_key: impl Into<String>) -> Self {
Self { api_key: api_key.into(), client: reqwest::Client::new() }
}
pub fn from_env() -> Result<Self, String> {
let key = std::env::var("ANTHROPIC_API_KEY")
.map_err(|_| "ANTHROPIC_API_KEY not set".to_string())?;
Ok(Self::new(key))
}
pub async fn complete(
&self,
model: &str,
system: Option<&str>,
user_message: &str,
max_tokens: u32,
) -> Result<CompletionResult, String> {
let request = MessagesRequest {
model: model.to_string(),
max_tokens,
system: system.map(String::from),
messages: vec![Message { role: "user".to_string(), content: user_message.to_string() }],
};
let response = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&request)
.send()
.await
.map_err(|e| format!("HTTP request failed: {e}"))?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "unable to read body".to_string());
return Err(format!("API error {status}: {body}"));
}
let resp: MessagesResponse =
response.json().await.map_err(|e| format!("Failed to parse response: {e}"))?;
let text = resp.content.first().map(|b| b.text.clone()).unwrap_or_default();
Ok(CompletionResult { text, usage: resp.usage })
}
}