doum_cli/llm/openai/
client.rs1use crate::system::error::{DoumError, Result};
2use crate::llm::client::{LLMClient, LLMRequest};
3use crate::llm::openai::payloads::{
4 OpenAIConfig, OpenAIRequest, OpenAIResponse, OpenAIOutput, OpenAIError, OpenAIWebSearchTool,
5};
6use reqwest::Client;
7use std::time::Duration;
8
9pub struct OpenAIClient {
11 http_client: Client,
12 config: OpenAIConfig,
13}
14
15impl OpenAIClient {
16 const API_URL: &'static str = "https://api.openai.com/v1/responses";
18
19 pub fn new(config: OpenAIConfig, timeout: u64) -> Result<Self> {
21 if config.api_key.is_empty() {
22 return Err(DoumError::InvalidConfig(
23 "OpenAI API key is not set. Please configure it in the interactive config menu (doum config).".to_string()
24 ));
25 }
26
27 let http_client = Client::builder()
28 .timeout(Duration::from_secs(timeout))
29 .build()
30 .map_err(|e| DoumError::LLM(format!("HTTP 클라이언트 생성 실패: {}", e)))?;
31
32 Ok(Self {
33 http_client,
34 config,
35 })
36 }
37}
38
39#[async_trait::async_trait]
40impl LLMClient for OpenAIClient {
41 async fn generate(&self, request: LLMRequest) -> Result<String> {
42 let openai_request = OpenAIRequest {
44 model: self.config.model.clone(),
45 instructions: Some(request.system),
46 input: request.messages,
47 tools: vec![
48 OpenAIWebSearchTool {
49 tool_type: "web_search".to_string(),
50 }
51 ].into(),
52 };
53
54 let mut builder = self.http_client
56 .post(Self::API_URL)
57 .header("Authorization", format!("Bearer {}", self.config.api_key))
58 .header("Content-Type", "application/json");
59
60 if let Some(ref org) = self.config.organization {
62 builder = builder.header("OpenAI-Organization", org);
63 }
64 if let Some(ref proj) = self.config.project {
65 builder = builder.header("OpenAI-Project", proj);
66 }
67
68 let response = builder
69 .json(&openai_request)
70 .send()
71 .await
72 .map_err(|e| {
73 if e.is_timeout() {
74 DoumError::Timeout
75 } else if e.is_connect() {
76 DoumError::LLM("네트워크 연결 실패. 인터넷 연결을 확인하세요.".to_string())
77 } else {
78 DoumError::LLM(format!("API 요청 실패: {}", e))
79 }
80 })?;
81
82 let status = response.status();
84
85 if !status.is_success() {
86 let error_text = response.text().await
87 .unwrap_or_else(|_| "알 수 없는 에러".to_string());
88
89 if let Ok(openai_error) = serde_json::from_str::<OpenAIError>(&error_text) {
91 return Err(DoumError::LLM(format!(
92 "OpenAI API 에러 ({}): {}",
93 status,
94 openai_error.error.message
95 )));
96 }
97
98 return Err(DoumError::LLM(format!(
99 "API 요청 실패 ({}): {}",
100 status,
101 error_text
102 )));
103 }
104
105 let openai_response: OpenAIResponse = response.json().await
107 .map_err(|e| DoumError::Parse(format!("응답 본문 읽기 실패: {}", e)))?;
108
109 for output in openai_response.output {
111 if let OpenAIOutput::Message { content } = output
112 && let Some(first_content) = content.first() {
113 return Ok(first_content.text.clone());
114 }
115 }
116
117 Err(DoumError::Parse(
118 "API 응답에 메시지 내용이 없습니다".to_string()
119 ))
120 }
121}