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 anyhow::{Context, Result};
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 anyhow::bail!(
23 "OpenAI API key is not set. Please configure it in the interactive config menu (doum config)."
24 );
25 }
26
27 let http_client = Client::builder()
28 .timeout(Duration::from_secs(timeout))
29 .build()
30 .context("Failed to build HTTP client")?;
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![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 anyhow::anyhow!("Request timeout")
72 } else if e.is_connect() {
73 anyhow::anyhow!("Failed to connect to OpenAI API")
74 } else {
75 anyhow::anyhow!("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 anyhow::bail!(
91 "OpenAI API Error ({}): {}",
92 status,
93 openai_error.error.message
94 );
95 }
96
97 anyhow::bail!("OpenAI API Error: {} - {}", status, error_text);
98 }
99
100 let openai_response: OpenAIResponse = response
102 .json()
103 .await
104 .context("Failed to parse OpenAI response")?;
105
106 for output in openai_response.output {
108 if let OpenAIOutput::Message { content } = output
109 && let Some(first_content) = content.first()
110 {
111 return Ok(first_content.text.clone());
112 }
113 }
114
115 anyhow::bail!("No content in OpenAI response")
116 }
117}