doum_cli/llm/anthropic/
client.rs

1use crate::system::error::{DoumError, Result};
2use crate::llm::client::{LLMClient, LLMRequest};
3use crate::llm::anthropic::payloads::{
4    AnthropicConfig, AnthropicRequest, AnthropicResponse, AnthropicError,
5};
6use reqwest::Client;
7use std::time::Duration;
8
9/// Anthropic 클라이언트
10pub struct AnthropicClient {
11    http_client: Client,
12    config: AnthropicConfig,
13}
14
15impl AnthropicClient {
16    /// Anthropic API 엔드포인트
17    const API_URL: &'static str = "https://api.anthropic.com/v1/messages";
18    const API_VERSION: &'static str = "2023-06-01";
19
20    /// 새 Anthropic 클라이언트 생성
21    pub fn new(config: AnthropicConfig, timeout: u64) -> Result<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!("HTTP 클라이언트 생성 실패: {}", 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) -> Result<String> {
43        // 요청 본문 구성
44        let request_body = AnthropicRequest {
45            model: self.config.model.clone(),
46            system: Some(request.system),
47            messages: request.messages,
48            max_tokens: 4096,
49        };
50
51        // API 요청
52        let response = self.http_client
53            .post(Self::API_URL)
54            .header("x-api-key", &self.config.api_key)
55            .header("anthropic-version", Self::API_VERSION)
56            .header("Content-Type", "application/json")
57            .json(&request_body)
58            .send()
59            .await
60            .map_err(|e| {
61                if e.is_timeout() {
62                    DoumError::Timeout
63                } else if e.is_connect() {
64                    DoumError::LLM("네트워크 연결 실패. 인터넷 연결을 확인하세요.".to_string())
65                } else {
66                    DoumError::LLM(format!("API 요청 실패: {}", e))
67                }
68            })?;
69
70        // HTTP 상태 코드 확인
71        let status = response.status();
72        
73        if !status.is_success() {
74            let error_text = response.text().await
75                .unwrap_or_else(|_| "알 수 없는 에러".to_string());
76            
77            // Anthropic 에러 응답 파싱 시도
78            if let Ok(anthropic_error) = serde_json::from_str::<AnthropicError>(&error_text) {
79                return Err(DoumError::LLM(format!(
80                    "Anthropic API 에러 ({}): {}",
81                    status,
82                    anthropic_error.error.message
83                )));
84            }
85            
86            return Err(DoumError::LLM(format!(
87                "API 요청 실패 ({}): {}",
88                status,
89                error_text
90            )));
91        }
92
93        // 응답 파싱
94        let anthropic_response: AnthropicResponse = response.json().await
95            .map_err(|e| DoumError::Parse(format!("API 응답 파싱 실패: {}", e)))?;
96
97        // 첫 번째 content block의 text 추출
98        anthropic_response.content
99            .first()
100            .map(|block| block.text.clone())
101            .ok_or_else(|| DoumError::Parse(
102                "API 응답에 컨텐츠가 없습니다".to_string()
103            ))
104    }
105}