Skip to main content

trueno_rag/eval/
client.rs

1//! Minimal Anthropic API client for eval operations
2//!
3//! No external anthropic crate — just reqwest + serde.
4//! Supports only the Messages API endpoint.
5
6use serde::{Deserialize, Serialize};
7
8/// Minimal Anthropic Messages API client
9#[derive(Debug, Clone)]
10pub struct AnthropicClient {
11    api_key: String,
12    client: reqwest::Client,
13}
14
15#[derive(Debug, Serialize)]
16struct MessagesRequest {
17    model: String,
18    max_tokens: u32,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    system: Option<String>,
21    messages: Vec<Message>,
22}
23
24#[derive(Debug, Serialize)]
25struct Message {
26    role: String,
27    content: String,
28}
29
30#[derive(Debug, Deserialize)]
31struct MessagesResponse {
32    content: Vec<ContentBlock>,
33    usage: Usage,
34}
35
36#[derive(Debug, Deserialize)]
37struct ContentBlock {
38    text: String,
39}
40
41/// Token usage from an API call
42#[derive(Debug, Deserialize)]
43pub struct Usage {
44    /// Input tokens consumed
45    pub input_tokens: u32,
46    /// Output tokens generated
47    pub output_tokens: u32,
48}
49
50/// Result of a single API call
51#[derive(Debug)]
52pub struct CompletionResult {
53    /// The text response
54    pub text: String,
55    /// Token usage
56    pub usage: Usage,
57}
58
59impl AnthropicClient {
60    /// Create a new client from an API key
61    pub fn new(api_key: impl Into<String>) -> Self {
62        Self { api_key: api_key.into(), client: reqwest::Client::new() }
63    }
64
65    /// Create a new client from the `ANTHROPIC_API_KEY` environment variable
66    pub fn from_env() -> Result<Self, String> {
67        let key = std::env::var("ANTHROPIC_API_KEY")
68            .map_err(|_| "ANTHROPIC_API_KEY not set".to_string())?;
69        Ok(Self::new(key))
70    }
71
72    /// Send a messages request and return the text response
73    pub async fn complete(
74        &self,
75        model: &str,
76        system: Option<&str>,
77        user_message: &str,
78        max_tokens: u32,
79    ) -> Result<CompletionResult, String> {
80        let request = MessagesRequest {
81            model: model.to_string(),
82            max_tokens,
83            system: system.map(String::from),
84            messages: vec![Message { role: "user".to_string(), content: user_message.to_string() }],
85        };
86
87        let response = self
88            .client
89            .post("https://api.anthropic.com/v1/messages")
90            .header("x-api-key", &self.api_key)
91            .header("anthropic-version", "2023-06-01")
92            .header("content-type", "application/json")
93            .json(&request)
94            .send()
95            .await
96            .map_err(|e| format!("HTTP request failed: {e}"))?;
97
98        let status = response.status();
99        if !status.is_success() {
100            let body = response.text().await.unwrap_or_else(|_| "unable to read body".to_string());
101            return Err(format!("API error {status}: {body}"));
102        }
103
104        let resp: MessagesResponse =
105            response.json().await.map_err(|e| format!("Failed to parse response: {e}"))?;
106
107        let text = resp.content.first().map(|b| b.text.clone()).unwrap_or_default();
108
109        Ok(CompletionResult { text, usage: resp.usage })
110    }
111}