doum_cli/llm/anthropic/
client.rs

1use crate::llm::anthropic::payloads::{
2    AnthropicConfig, AnthropicError, AnthropicRequest, AnthropicResponse,
3};
4use crate::llm::client::{LLMClient, LLMRequest};
5use crate::system::error::{DoumError, DoumResult};
6use reqwest::Client;
7use std::time::Duration;
8
9/// Anthropic LLM Client
10pub struct AnthropicClient {
11    http_client: Client,
12    config: AnthropicConfig,
13}
14
15impl AnthropicClient {
16    /// Anthropic API URL and Version
17    const API_URL: &'static str = "https://api.anthropic.com/v1/messages";
18    const API_VERSION: &'static str = "2023-06-01";
19
20    /// Create a new AnthropicClient
21    pub fn new(config: AnthropicConfig, timeout: u64) -> DoumResult<Self> {
22        if config.api_key.is_empty() {
23            return Err(DoumError::InvalidConfig(
24                "Anthropic API key is not set. Please configure it in the interactive config menu (doum config).".to_string()
25            ));
26        }
27
28        let http_client = Client::builder()
29            .timeout(Duration::from_secs(timeout))
30            .build()
31            .map_err(|e| DoumError::LLM(format!("Failed to build HTTP client: {}", e)))?;
32
33        Ok(Self {
34            http_client,
35            config,
36        })
37    }
38}
39
40#[async_trait::async_trait]
41impl LLMClient for AnthropicClient {
42    async fn generate(&self, request: LLMRequest) -> DoumResult<String> {
43        let request_body = AnthropicRequest {
44            model: self.config.model.clone(),
45            system: Some(request.system),
46            messages: request.messages,
47            max_tokens: 4096,
48        };
49
50        let response = self
51            .http_client
52            .post(Self::API_URL)
53            .header("x-api-key", &self.config.api_key)
54            .header("anthropic-version", Self::API_VERSION)
55            .header("Content-Type", "application/json")
56            .json(&request_body)
57            .send()
58            .await
59            .map_err(|e| {
60                if e.is_timeout() {
61                    DoumError::Timeout
62                } else if e.is_connect() {
63                    DoumError::LLM("Failed to connect to Anthropic API".to_string())
64                } else {
65                    DoumError::LLM(format!("Failed to send request to Anthropic API: {}", e))
66                }
67            })?;
68
69        // Check response status
70        let status = response.status();
71
72        if !status.is_success() {
73            let error_text = response
74                .text()
75                .await
76                .unwrap_or_else(|_| "Unknown error".to_string());
77
78            // Try to parse Anthropic error format
79            if let Ok(anthropic_error) = serde_json::from_str::<AnthropicError>(&error_text) {
80                return Err(DoumError::LLM(format!(
81                    "Anthropic API Error ({}): {}",
82                    status, anthropic_error.error.message
83                )));
84            }
85
86            return Err(DoumError::LLM(format!(
87                "Anthropic API Error: {} - {}",
88                status, error_text
89            )));
90        }
91
92        // Parse response body
93        let anthropic_response: AnthropicResponse = response
94            .json()
95            .await
96            .map_err(|e| DoumError::Parse(format!("Failed to parse Anthropic response: {}", e)))?;
97
98        // Extract and return the generated content
99        anthropic_response
100            .content
101            .first()
102            .map(|block| block.text.clone())
103            .ok_or_else(|| DoumError::Parse("No content in Anthropic response".to_string()))
104    }
105}