doum_cli/llm/anthropic/
client.rs1use 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
9pub struct AnthropicClient {
11 http_client: Client,
12 config: AnthropicConfig,
13}
14
15impl AnthropicClient {
16 const API_URL: &'static str = "https://api.anthropic.com/v1/messages";
18 const API_VERSION: &'static str = "2023-06-01";
19
20 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 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 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 let anthropic_response: AnthropicResponse = response
94 .json()
95 .await
96 .map_err(|e| DoumError::Parse(format!("Failed to parse Anthropic response: {}", e)))?;
97
98 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}