doum_cli/llm/anthropic/
client.rs1use crate::llm::anthropic::payloads::{
2 AnthropicConfig, AnthropicError, AnthropicRequest, AnthropicResponse,
3};
4use crate::llm::client::{LLMClient, LLMRequest};
5use anyhow::{Context, Result};
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) -> Result<Self> {
22 if config.api_key.is_empty() {
23 anyhow::bail!(
24 "Anthropic API key is not set. Please configure it in the interactive config menu (doum config)."
25 );
26 }
27
28 let http_client = Client::builder()
29 .timeout(Duration::from_secs(timeout))
30 .build()
31 .context("Failed to build HTTP client")?;
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) -> Result<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 anyhow::anyhow!("Request timeout")
62 } else if e.is_connect() {
63 anyhow::anyhow!("Failed to connect to Anthropic API")
64 } else {
65 anyhow::anyhow!("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 anyhow::bail!(
81 "Anthropic API Error ({}): {}",
82 status,
83 anthropic_error.error.message
84 );
85 }
86
87 anyhow::bail!("Anthropic API Error: {} - {}", status, error_text);
88 }
89
90 let anthropic_response: AnthropicResponse = response
92 .json()
93 .await
94 .context("Failed to parse Anthropic response")?;
95
96 anthropic_response
98 .content
99 .first()
100 .map(|block| block.text.clone())
101 .ok_or_else(|| anyhow::anyhow!("No content in Anthropic response"))
102 }
103}