asimov_anthropic_module/
lib.rs1#![no_std]
4#![forbid(unsafe_code)]
5
6use asimov_module::{
7 prelude::*,
8 secrecy::{ExposeSecret, SecretString},
9 tracing,
10};
11use core::error::Error;
12use serde_json::{Value, json};
13
14#[derive(Clone, Debug, bon::Builder)]
15#[builder(on(String, into))]
16pub struct Options {
17 #[builder(default = "https://api.anthropic.com")]
18 pub endpoint: String,
19
20 #[builder(default = "claude-opus-4-1-20250805")]
21 pub model: String,
22
23 #[builder(default = 1024)]
24 pub max_tokens: usize,
25
26 #[builder(into)]
27 pub api_key: SecretString,
28}
29
30pub fn chat(input: impl AsRef<str>, options: &Options) -> Result<Vec<String>, Box<dyn Error>> {
31 let req = json!({
32 "model": options.model,
33 "max_tokens": options.max_tokens,
34 "messages": [
35 {"role": "user", "content": input.as_ref() }
36 ]
37 });
38
39 let mut resp = ureq::Agent::config_builder()
40 .http_status_as_error(false)
41 .user_agent("asimov-anthropic-module")
42 .build()
43 .new_agent()
44 .post(options.endpoint.clone() + "/v1/messages")
45 .header("x-api-key", options.api_key.expose_secret())
46 .header("anthropic-version", "2023-06-01")
47 .header("content-type", "application/json")
48 .send_json(&req)
49 .inspect_err(|e| tracing::error!("HTTP request failed: {e}"))?;
50 tracing::debug!(response = ?resp);
51
52 let status = resp.status();
53 tracing::debug!(status = status.to_string());
54
55 let resp: Value = resp
56 .body_mut()
57 .read_json()
58 .inspect_err(|e| tracing::error!("unable to read HTTP response body: {e}"))?;
59 tracing::debug!(body = ?resp);
60
61 if !status.is_success() {
62 tracing::error!("Received an error response: {status}");
63
64 if let Some(message) = resp["error"]["message"].as_str() {
73 return Err(message.into());
74 }
75 }
76
77 if let Some(stop_reason) = resp["stop_reason"].as_str() {
104 tracing::debug!(stop_reason);
105 }
106
107 let responses = resp["content"]
108 .as_array()
109 .into_iter()
110 .flatten()
111 .filter(|c| c["type"].as_str().is_some_and(|t| t == "text"))
112 .flat_map(|c| c["text"].as_str().map(ToString::to_string))
113 .collect();
114
115 Ok(responses)
116}