doum_cli/llm/openai/
client.rs1use crate::llm::client::{LLMClient, LLMRequest};
2use crate::llm::openai::payloads::{
3 OpenAIConfig, OpenAIError, OpenAIOutput, OpenAIRequest, OpenAIResponse, OpenAIWebSearchTool,
4};
5use crate::system::error::{DoumError, DoumResult};
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) -> DoumResult<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!("Failed to build HTTP client: {}", 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) -> DoumResult<String> {
42 let openai_request = OpenAIRequest {
44 model: self.config.model.clone(),
45 instructions: Some(request.system),
46 input: request.messages,
47 tools: vec![OpenAIWebSearchTool {
48 tool_type: "web_search".to_string(),
49 }]
50 .into(),
51 };
52
53 let mut builder = self
55 .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.json(&openai_request).send().await.map_err(|e| {
70 if e.is_timeout() {
71 DoumError::Timeout
72 } else if e.is_connect() {
73 DoumError::LLM("Failed to connect to OpenAI API".to_string())
74 } else {
75 DoumError::LLM(format!("Failed to send request to OpenAI API: {}", e))
76 }
77 })?;
78
79 let status = response.status();
81
82 if !status.is_success() {
83 let error_text = response
84 .text()
85 .await
86 .unwrap_or_else(|_| "Unknown error".to_string());
87
88 if let Ok(openai_error) = serde_json::from_str::<OpenAIError>(&error_text) {
90 return Err(DoumError::LLM(format!(
91 "OpenAI API Error ({}): {}",
92 status, openai_error.error.message
93 )));
94 }
95
96 return Err(DoumError::LLM(format!(
97 "OpenAI API Error: {} - {}",
98 status, error_text
99 )));
100 }
101
102 let openai_response: OpenAIResponse = response
104 .json()
105 .await
106 .map_err(|e| DoumError::Parse(format!("Failed to parse OpenAI response: {}", e)))?;
107
108 for output in openai_response.output {
110 if let OpenAIOutput::Message { content } = output
111 && let Some(first_content) = content.first()
112 {
113 return Ok(first_content.text.clone());
114 }
115 }
116
117 Err(DoumError::Parse(
118 "No content in OpenAI response".to_string(),
119 ))
120 }
121}