use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use crate::AiConfig;
#[derive(Serialize)]
struct MessagesRequest<'a> {
model: &'a str,
max_tokens: u32,
system: &'a str,
messages: Vec<Message<'a>>,
}
#[derive(Serialize)]
struct Message<'a> {
role: &'a str,
content: &'a str,
}
#[derive(Deserialize)]
struct MessagesResponse {
content: Vec<ContentBlock>,
}
#[derive(Deserialize)]
struct ContentBlock {
#[serde(default)]
text: Option<String>,
}
pub(crate) async fn complete(
client: &reqwest::Client,
config: &AiConfig,
system: &str,
user: &str,
) -> Result<String> {
let base = config
.base_url
.as_deref()
.unwrap_or("https://api.anthropic.com");
let url = format!("{base}/v1/messages");
let body = MessagesRequest {
model: &config.model,
max_tokens: config.max_tokens,
system,
messages: vec![Message {
role: "user",
content: user,
}],
};
let resp = client
.post(&url)
.header("x-api-key", config.api_key())
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.timeout(std::time::Duration::from_secs(config.timeout_secs))
.json(&body)
.send()
.await
.context("Failed to reach Anthropic API")?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
bail!("Anthropic API error ({status}): {text}");
}
let parsed: MessagesResponse = resp.json().await.context("Failed to parse response")?;
parsed
.content
.into_iter()
.find_map(|c| c.text)
.context("No text content in Anthropic response")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_serialization() {
let req = MessagesRequest {
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: "You are helpful.",
messages: vec![Message {
role: "user",
content: "Hello",
}],
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("claude-sonnet"));
assert!(json.contains("\"role\":\"user\""));
}
#[test]
fn response_deserialization() {
let json = r#"{"content":[{"type":"text","text":"Hello back!"}]}"#;
let resp: MessagesResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.content[0].text.as_deref(), Some("Hello back!"));
}
}