use serde::{Deserialize, Serialize};
use super::prompts;
const DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
const ANTHROPIC_VERSION: &str = "2023-06-01";
const DEFAULT_MODEL: &str = "claude-sonnet-4-5";
const MAX_TOKENS: u32 = 4096;
#[derive(Serialize)]
struct MessagesRequest<'a> {
model: &'a str,
max_tokens: u32,
system: String,
messages: Vec<Message>,
}
#[derive(Serialize)]
struct Message {
role: &'static str,
content: String,
}
#[derive(Deserialize)]
struct MessagesResponse {
content: Vec<ContentBlock>,
}
#[derive(Deserialize)]
struct ContentBlock {
#[serde(default)]
text: String,
#[serde(rename = "type", default)]
block_type: String,
}
pub async fn request(api_key: &str, prose: &str) -> Result<String, String> {
send(
api_key,
prompts::system_prompt(),
prompts::build_user_prompt(prose),
)
.await
}
pub async fn request_update(
api_key: &str,
existing_json: &str,
instruction: &str,
) -> Result<String, String> {
send(
api_key,
prompts::system_prompt_update(),
prompts::build_user_update_prompt(existing_json, instruction),
)
.await
}
pub async fn request_analyze(api_key: &str, existing_json: &str) -> Result<String, String> {
send(
api_key,
prompts::system_prompt_analyze(),
prompts::build_user_analyze_prompt(existing_json),
)
.await
}
pub async fn request_explain(
api_key: &str,
old_json: &str,
new_json: &str,
) -> Result<String, String> {
send(
api_key,
prompts::system_prompt_explain(),
prompts::build_user_explain_prompt(old_json, new_json),
)
.await
}
async fn send(api_key: &str, system: String, user: String) -> Result<String, String> {
let base =
std::env::var("ANTHROPIC_API_BASE").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string());
let model = std::env::var("RUSTIO_AI_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
let body = MessagesRequest {
model: &model,
max_tokens: MAX_TOKENS,
system,
messages: vec![Message {
role: "user",
content: user,
}],
};
let url = format!("{base}/v1/messages");
let client = reqwest::Client::new();
let resp = client
.post(&url)
.header("x-api-key", api_key)
.header("anthropic-version", ANTHROPIC_VERSION)
.header("content-type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| format!("request to {url} failed: {e}"))?;
let status = resp.status();
let raw = resp
.text()
.await
.map_err(|e| format!("read response body: {e}"))?;
if !status.is_success() {
return Err(format!("HTTP {status}: {raw}"));
}
let parsed: MessagesResponse = serde_json::from_str(&raw)
.map_err(|e| format!("invalid Anthropic response (HTTP {status}): {e}; body: {raw}"))?;
let text = parsed
.content
.into_iter()
.find(|b| b.block_type == "text" && !b.text.is_empty())
.ok_or_else(|| "Anthropic response had no text content block".to_string())?
.text;
Ok(text)
}