1use super::openai_shared::{
6 http::OpenAICompatibleClient, utils::apply_config_to_request, OpenAIRequest, OpenAIResponse,
7};
8use crate::config::{DefaultLLMParams, OpenAIConfig};
10use crate::error::{LlmError, LlmResult};
11#[cfg(feature = "events")]
12use crate::internals::events::{event_types, BusinessEvent, EventScope};
13use crate::internals::response_parser::ResponseParser;
14use crate::logging::{log_debug, log_error};
15use crate::messages::{MessageContent, MessageRole, UnifiedLLMRequest, UnifiedMessage};
16#[cfg(feature = "events")]
17use crate::provider::LLMBusinessEvent;
18use crate::provider::{LlmProvider, RequestConfig, Response, TokenUsage, ToolCallingRound};
19use std::time::Instant;
20
21#[derive(Debug)]
23pub struct OpenAIProvider {
24 http_client: OpenAICompatibleClient,
25 config: OpenAIConfig,
26 default_params: DefaultLLMParams,
27}
28
29impl OpenAIProvider {
30 pub fn new(config: OpenAIConfig, default_params: DefaultLLMParams) -> LlmResult<Self> {
39 log_debug!(
40 provider = "openai",
41 has_api_key = config.api_key.is_some(),
42 max_context_tokens = config.max_context_tokens,
43 base_url = %config.base_url,
44 default_model = %config.default_model,
45 default_temperature = default_params.temperature,
46 "Creating OpenAI provider"
47 );
48
49 if config.api_key.is_none() {
50 return Err(LlmError::configuration_error("OpenAI API key is required"));
51 }
52
53 log_debug!(
54 provider = "openai",
55 max_context_tokens = config.max_context_tokens,
56 default_model = %config.default_model,
57 default_temperature = default_params.temperature,
58 "OpenAI provider initialized"
59 );
60
61 Ok(Self {
62 http_client: OpenAICompatibleClient::with_retry_policy(config.retry_policy.clone()),
63 config,
64 default_params,
65 })
66 }
67
68 pub(crate) async fn restore_default_retry_policy(&self) {
70 }
73
74 fn create_base_request(&self, request: &UnifiedLLMRequest) -> OpenAIRequest {
76 let openai_messages = self.transform_unified_messages(&request.get_sorted_messages());
77
78 OpenAIRequest {
79 model: self.config.default_model.clone(),
80 messages: openai_messages,
81 temperature: Some(self.default_params.temperature),
82 max_tokens: Some(self.default_params.max_tokens),
83 top_p: Some(self.default_params.top_p),
84 presence_penalty: None,
85 stream: None,
86 tools: None,
87 tool_choice: None,
88 response_format: None,
89 }
90 }
91
92 fn apply_response_schema(
94 &self,
95 request: &mut OpenAIRequest,
96 schema: Option<serde_json::Value>,
97 ) {
98 if let Some(schema) = schema {
99 request.response_format = Some(super::openai_shared::OpenAIResponseFormat {
100 format_type: "json_schema".to_string(),
101 json_schema: Some(super::openai_shared::OpenAIJsonSchema {
102 name: "structured_response".to_string(),
103 schema,
104 strict: Some(true),
105 }),
106 });
107 }
108 }
109
110 async fn send_openai_request(
112 &self,
113 request: &OpenAIRequest,
114 ) -> crate::provider::Result<OpenAIResponse> {
115 let url = format!("{}/v1/chat/completions", self.config.base_url);
117
118 let headers = OpenAICompatibleClient::build_auth_headers(
119 self.config.api_key.as_ref().unwrap_or(&String::new()),
120 )?;
121 self.http_client
122 .execute_chat_request(&url, &headers, request)
123 .await
124 }
125
126 #[cfg(feature = "events")]
128 fn create_request_event(
129 &self,
130 request: &OpenAIRequest,
131 config: Option<&RequestConfig>,
132 ) -> Option<LLMBusinessEvent> {
133 let user_id = config.and_then(|c| c.user_id.clone())?;
134
135 let event = BusinessEvent::new(event_types::LLM_REQUEST)
136 .with_metadata("provider", "openai")
137 .with_metadata("model", &request.model);
138
139 Some(LLMBusinessEvent {
140 event,
141 scope: EventScope::User(user_id),
142 })
143 }
144
145 #[cfg(feature = "events")]
147 fn create_error_event(
148 &self,
149 error: &str,
150 config: Option<&RequestConfig>,
151 ) -> Option<LLMBusinessEvent> {
152 let user_id = config.and_then(|c| c.user_id.clone())?;
153
154 let event = BusinessEvent::new(event_types::LLM_ERROR)
155 .with_metadata("provider", "openai")
156 .with_metadata("error", error);
157
158 Some(LLMBusinessEvent {
159 event,
160 scope: EventScope::User(user_id),
161 })
162 }
163
164 #[cfg(feature = "events")]
166 fn create_response_event(
167 &self,
168 api_response: &OpenAIResponse,
169 duration_ms: u64,
170 config: Option<&RequestConfig>,
171 ) -> Option<LLMBusinessEvent> {
172 let user_id = config.and_then(|c| c.user_id.clone())?;
173
174 let usage_tokens = api_response
175 .usage
176 .as_ref()
177 .map(|u| (u.prompt_tokens, u.completion_tokens));
178 let mut event = BusinessEvent::new(event_types::LLM_RESPONSE)
179 .with_metadata("provider", "openai")
180 .with_metadata("model", &self.config.default_model)
181 .with_metadata("input_tokens", usage_tokens.map(|(i, _)| i).unwrap_or(0))
182 .with_metadata("output_tokens", usage_tokens.map(|(_, o)| o).unwrap_or(0))
183 .with_metadata("duration_ms", duration_ms);
184
185 if let Some(ref sess_id) = config.and_then(|c| c.session_id.as_ref()) {
186 event = event.with_metadata("session_id", sess_id);
187 }
188
189 Some(LLMBusinessEvent {
190 event,
191 scope: EventScope::User(user_id),
192 })
193 }
194
195 async fn execute_llm_internal(
199 &self,
200 request: UnifiedLLMRequest,
201 config: Option<RequestConfig>,
202 ) -> crate::provider::Result<(Response, OpenAIResponse, u64, OpenAIRequest)> {
203 let mut openai_request = self.create_base_request(&request);
205 if let Some(cfg) = config.as_ref() {
206 apply_config_to_request(&mut openai_request, Some(cfg.clone()));
207 }
208 self.apply_response_schema(&mut openai_request, request.response_schema);
209
210 log_debug!(
211 provider = "openai",
212 request_json = %serde_json::to_string(&openai_request).unwrap_or_default(),
213 "Executing LLM request"
214 );
215
216 let openai_request_for_events = openai_request.clone();
218
219 let start_time = Instant::now();
221 let api_response = self.send_openai_request(&openai_request).await?;
222 let duration_ms = start_time.elapsed().as_millis() as u64;
223
224 let response = self.parse_openai_response(api_response.clone())?;
226
227 Ok((
228 response,
229 api_response,
230 duration_ms,
231 openai_request_for_events,
232 ))
233 }
234
235 fn transform_unified_messages(
238 &self,
239 messages: &[&UnifiedMessage],
240 ) -> Vec<super::openai_shared::OpenAIMessage> {
241 messages
242 .iter()
243 .map(|msg| self.unified_message_to_openai(msg))
244 .collect()
245 }
246
247 fn unified_message_to_openai(
250 &self,
251 msg: &UnifiedMessage,
252 ) -> super::openai_shared::OpenAIMessage {
253 let role = match msg.role {
254 MessageRole::System => "system".to_string(),
255 MessageRole::User => "user".to_string(),
256 MessageRole::Assistant => "assistant".to_string(),
257 MessageRole::Tool => "tool".to_string(),
258 };
259
260 let content = match &msg.content {
261 MessageContent::Text(text) => text.clone(),
262 MessageContent::Json(value) => serde_json::to_string_pretty(value).unwrap_or_default(),
263 MessageContent::ToolCall { .. } => {
264 log_error!(provider = "openai", "Unexpected ToolCall in outgoing message - tool calls are received from LLM, not sent to it");
266 "Error: Invalid message type".to_string()
267 }
268 MessageContent::ToolResult {
269 tool_call_id: _,
270 content,
271 is_error,
272 } => {
273 if *is_error {
274 format!("Error: {}", content)
275 } else {
276 content.clone()
277 }
278 }
279 };
280
281 super::openai_shared::OpenAIMessage { role, content }
282 }
283
284 fn parse_openai_response(&self, response: OpenAIResponse) -> LlmResult<Response> {
286 let choice = response
287 .choices
288 .into_iter()
289 .next()
290 .ok_or_else(|| LlmError::response_parsing_error("No choices in OpenAI response"))?;
291
292 let content = choice.message.content;
293
294 let tool_calls = choice
295 .message
296 .tool_calls
297 .unwrap_or_default()
298 .into_iter()
299 .map(|tc| crate::provider::ToolCall {
300 id: tc.id,
301 name: tc.function.name,
302 arguments: serde_json::from_str(&tc.function.arguments).unwrap_or_default(),
303 })
304 .collect();
305
306 let usage = response.usage.map(|u| TokenUsage {
307 prompt_tokens: u.prompt_tokens,
308 completion_tokens: u.completion_tokens,
309 total_tokens: u.total_tokens,
310 });
311
312 let structured_response = if content.trim_start().starts_with('{') {
314 match ResponseParser::parse_llm_output(&content) {
315 Ok(json_value) => {
316 log_debug!(
317 provider = "openai",
318 "Successfully parsed structured JSON response"
319 );
320 Some(json_value)
321 }
322 Err(_) => None,
323 }
324 } else {
325 None
326 };
327
328 Ok(Response {
329 content,
330 structured_response,
331 tool_calls,
332 usage,
333 model: Some(self.config.default_model.clone()),
334 raw_body: None,
335 })
336 }
337}
338
339#[async_trait::async_trait]
340impl LlmProvider for OpenAIProvider {
341 #[cfg(feature = "events")]
342 async fn execute_llm(
343 &self,
344 request: UnifiedLLMRequest,
345 _current_tool_round: Option<ToolCallingRound>,
346 config: Option<RequestConfig>,
347 ) -> crate::provider::Result<(Response, Vec<LLMBusinessEvent>)> {
348 let mut events = Vec::new();
349
350 let (response, api_response, duration_ms, openai_request) =
352 match self.execute_llm_internal(request, config.clone()).await {
353 Ok(result) => result,
354 Err(e) => {
355 if let Some(event) = self.create_error_event(&e.to_string(), config.as_ref()) {
357 events.push(event);
358 }
359 return Err(e);
360 }
361 };
362
363 if let Some(event) = self.create_request_event(&openai_request, config.as_ref()) {
365 events.push(event);
366 }
367
368 if let Some(event) = self.create_response_event(&api_response, duration_ms, config.as_ref())
370 {
371 events.push(event);
372 }
373
374 Ok((response, events))
375 }
376
377 #[cfg(not(feature = "events"))]
378 async fn execute_llm(
379 &self,
380 request: UnifiedLLMRequest,
381 _current_tool_round: Option<ToolCallingRound>,
382 config: Option<RequestConfig>,
383 ) -> crate::provider::Result<Response> {
384 let (response, _api_response, _duration_ms, _openai_request) =
386 self.execute_llm_internal(request, config).await?;
387 Ok(response)
388 }
389
390 #[cfg(feature = "events")]
391 async fn execute_structured_llm(
392 &self,
393 mut request: UnifiedLLMRequest,
394 current_tool_round: Option<ToolCallingRound>,
395 schema: serde_json::Value,
396 config: Option<RequestConfig>,
397 ) -> crate::provider::Result<(Response, Vec<LLMBusinessEvent>)> {
398 request.response_schema = Some(schema);
400
401 self.execute_llm(request, current_tool_round, config).await
403 }
404
405 #[cfg(not(feature = "events"))]
406 async fn execute_structured_llm(
407 &self,
408 mut request: UnifiedLLMRequest,
409 current_tool_round: Option<ToolCallingRound>,
410 schema: serde_json::Value,
411 config: Option<RequestConfig>,
412 ) -> crate::provider::Result<Response> {
413 request.response_schema = Some(schema);
415
416 self.execute_llm(request, current_tool_round, config).await
418 }
419
420 fn provider_name(&self) -> &'static str {
421 "openai"
422 }
423}