#![no_std]
#![forbid(unsafe_code)]
use asimov_module::{
prelude::*,
secrecy::{ExposeSecret, SecretString},
tracing,
};
use core::error::Error;
use serde_json::{Value, json};
#[derive(Clone, Debug, bon::Builder)]
#[builder(on(String, into))]
pub struct Options {
#[builder(default = "https://api.anthropic.com")]
pub endpoint: String,
#[builder(default = "claude-opus-4-1-20250805")]
pub model: String,
#[builder(default = 1024)]
pub max_tokens: usize,
#[builder(into)]
pub api_key: SecretString,
}
pub fn chat(input: impl AsRef<str>, options: &Options) -> Result<Vec<String>, Box<dyn Error>> {
let req = json!({
"model": options.model,
"max_tokens": options.max_tokens,
"messages": [
{"role": "user", "content": input.as_ref() }
]
});
let mut resp = ureq::Agent::config_builder()
.http_status_as_error(false)
.user_agent("asimov-anthropic-module")
.build()
.new_agent()
.post(options.endpoint.clone() + "/v1/messages")
.header("x-api-key", options.api_key.expose_secret())
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.send_json(&req)
.inspect_err(|e| tracing::error!("HTTP request failed: {e}"))?;
tracing::debug!(response = ?resp);
let status = resp.status();
tracing::debug!(status = status.to_string());
let resp: Value = resp
.body_mut()
.read_json()
.inspect_err(|e| tracing::error!("unable to read HTTP response body: {e}"))?;
tracing::debug!(body = ?resp);
if !status.is_success() {
tracing::error!("Received an error response: {status}");
if let Some(message) = resp["error"]["message"].as_str() {
return Err(message.into());
}
}
if let Some(stop_reason) = resp["stop_reason"].as_str() {
tracing::debug!(stop_reason);
}
let responses = resp["content"]
.as_array()
.into_iter()
.flatten()
.filter(|c| c["type"].as_str().is_some_and(|t| t == "text"))
.flat_map(|c| c["text"].as_str().map(ToString::to_string))
.collect();
Ok(responses)
}