doum_cli/llm/anthropic/
client.rs1use 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
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 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 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 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 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 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 let anthropic_response: AnthropicResponse = response.json().await
95 .map_err(|e| DoumError::Parse(format!("API 응답 파싱 실패: {}", e)))?;
96
97 anthropic_response.content
99 .first()
100 .map(|block| block.text.clone())
101 .ok_or_else(|| DoumError::Parse(
102 "API 응답에 컨텐츠가 없습니다".to_string()
103 ))
104 }
105}