vtcode_core/llm/providers/
openai.rs

1use crate::config::constants::{models, urls};
2use crate::config::core::{OpenAIPromptCacheSettings, PromptCachingConfig};
3use crate::llm::client::LLMClient;
4use crate::llm::error_display;
5use crate::llm::provider::{
6    FinishReason, LLMError, LLMProvider, LLMRequest, LLMResponse, Message, MessageRole, ToolCall,
7    ToolChoice, ToolDefinition,
8};
9use crate::llm::types as llm_types;
10use async_trait::async_trait;
11use reqwest::Client as HttpClient;
12use serde_json::{Value, json};
13
14use super::{extract_reasoning_trace, gpt5_codex_developer_prompt};
15
16pub struct OpenAIProvider {
17    api_key: String,
18    http_client: HttpClient,
19    base_url: String,
20    model: String,
21    prompt_cache_enabled: bool,
22    prompt_cache_settings: OpenAIPromptCacheSettings,
23}
24
25impl OpenAIProvider {
26    fn is_gpt5_codex_model(model: &str) -> bool {
27        model == models::openai::GPT_5_CODEX
28    }
29
30    fn is_reasoning_model(model: &str) -> bool {
31        models::openai::REASONING_MODELS
32            .iter()
33            .any(|candidate| *candidate == model)
34    }
35
36    fn uses_responses_api(model: &str) -> bool {
37        Self::is_gpt5_codex_model(model)
38    }
39
40    pub fn new(api_key: String) -> Self {
41        Self::with_model_internal(api_key, models::openai::DEFAULT_MODEL.to_string(), None)
42    }
43
44    pub fn with_model(api_key: String, model: String) -> Self {
45        Self::with_model_internal(api_key, model, None)
46    }
47
48    pub fn from_config(
49        api_key: Option<String>,
50        model: Option<String>,
51        base_url: Option<String>,
52        prompt_cache: Option<PromptCachingConfig>,
53    ) -> Self {
54        let api_key_value = api_key.unwrap_or_default();
55        let mut provider = if let Some(model_value) = model {
56            Self::with_model_internal(api_key_value, model_value, prompt_cache)
57        } else {
58            Self::with_model_internal(
59                api_key_value,
60                models::openai::DEFAULT_MODEL.to_string(),
61                prompt_cache,
62            )
63        };
64        if let Some(base) = base_url {
65            provider.base_url = base;
66        }
67        provider
68    }
69
70    fn with_model_internal(
71        api_key: String,
72        model: String,
73        prompt_cache: Option<PromptCachingConfig>,
74    ) -> Self {
75        let (prompt_cache_enabled, prompt_cache_settings) =
76            Self::extract_prompt_cache_settings(prompt_cache);
77
78        Self {
79            api_key,
80            http_client: HttpClient::new(),
81            base_url: urls::OPENAI_API_BASE.to_string(),
82            model,
83            prompt_cache_enabled,
84            prompt_cache_settings,
85        }
86    }
87
88    fn extract_prompt_cache_settings(
89        prompt_cache: Option<PromptCachingConfig>,
90    ) -> (bool, OpenAIPromptCacheSettings) {
91        if let Some(cfg) = prompt_cache {
92            let provider_settings = cfg.providers.openai;
93            let enabled = cfg.enabled && provider_settings.enabled;
94            (enabled, provider_settings)
95        } else {
96            (false, OpenAIPromptCacheSettings::default())
97        }
98    }
99
100    fn supports_temperature_parameter(model: &str) -> bool {
101        // GPT-5 variants and GPT-5 Codex models don't support temperature parameter
102        // All other OpenAI models generally support it
103        !Self::is_gpt5_codex_model(model)
104            && model != models::openai::GPT_5
105            && model != models::openai::GPT_5_MINI
106            && model != models::openai::GPT_5_NANO
107    }
108
109    fn default_request(&self, prompt: &str) -> LLMRequest {
110        LLMRequest {
111            messages: vec![Message::user(prompt.to_string())],
112            system_prompt: None,
113            tools: None,
114            model: self.model.clone(),
115            max_tokens: None,
116            temperature: None,
117            stream: false,
118            tool_choice: None,
119            parallel_tool_calls: None,
120            parallel_tool_config: None,
121            reasoning_effort: None,
122        }
123    }
124
125    fn parse_client_prompt(&self, prompt: &str) -> LLMRequest {
126        let trimmed = prompt.trim_start();
127        if trimmed.starts_with('{') {
128            if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
129                if let Some(request) = self.parse_chat_request(&value) {
130                    return request;
131                }
132            }
133        }
134
135        self.default_request(prompt)
136    }
137
138    fn parse_chat_request(&self, value: &Value) -> Option<LLMRequest> {
139        let messages_value = value.get("messages")?.as_array()?;
140        let mut system_prompt = None;
141        let mut messages = Vec::new();
142
143        for entry in messages_value {
144            let role = entry
145                .get("role")
146                .and_then(|r| r.as_str())
147                .unwrap_or(crate::config::constants::message_roles::USER);
148            let content = entry.get("content");
149            let text_content = content.map(Self::extract_content_text).unwrap_or_default();
150
151            match role {
152                "system" => {
153                    if system_prompt.is_none() && !text_content.is_empty() {
154                        system_prompt = Some(text_content);
155                    }
156                }
157                "assistant" => {
158                    let tool_calls = entry
159                        .get("tool_calls")
160                        .and_then(|tc| tc.as_array())
161                        .map(|calls| {
162                            calls
163                                .iter()
164                                .filter_map(|call| {
165                                    let id = call.get("id").and_then(|v| v.as_str())?;
166                                    let function = call.get("function")?;
167                                    let name = function.get("name").and_then(|v| v.as_str())?;
168                                    let arguments = function.get("arguments");
169                                    let serialized = arguments.map_or("{}".to_string(), |value| {
170                                        if value.is_string() {
171                                            value.as_str().unwrap_or("").to_string()
172                                        } else {
173                                            value.to_string()
174                                        }
175                                    });
176                                    Some(ToolCall::function(
177                                        id.to_string(),
178                                        name.to_string(),
179                                        serialized,
180                                    ))
181                                })
182                                .collect::<Vec<_>>()
183                        })
184                        .filter(|calls| !calls.is_empty());
185
186                    let message = if let Some(calls) = tool_calls {
187                        Message {
188                            role: MessageRole::Assistant,
189                            content: text_content,
190                            tool_calls: Some(calls),
191                            tool_call_id: None,
192                        }
193                    } else {
194                        Message::assistant(text_content)
195                    };
196                    messages.push(message);
197                }
198                "tool" => {
199                    let tool_call_id = entry
200                        .get("tool_call_id")
201                        .and_then(|id| id.as_str())
202                        .map(|s| s.to_string());
203                    let content_value = entry
204                        .get("content")
205                        .map(|value| {
206                            if text_content.is_empty() {
207                                value.to_string()
208                            } else {
209                                text_content.clone()
210                            }
211                        })
212                        .unwrap_or_else(|| text_content.clone());
213                    messages.push(Message {
214                        role: MessageRole::Tool,
215                        content: content_value,
216                        tool_calls: None,
217                        tool_call_id,
218                    });
219                }
220                _ => {
221                    messages.push(Message::user(text_content));
222                }
223            }
224        }
225
226        if messages.is_empty() {
227            return None;
228        }
229
230        let tools = value.get("tools").and_then(|tools_value| {
231            let tools_array = tools_value.as_array()?;
232            let converted: Vec<_> = tools_array
233                .iter()
234                .filter_map(|tool| {
235                    let function = tool.get("function")?;
236                    let name = function.get("name").and_then(|n| n.as_str())?;
237                    let description = function
238                        .get("description")
239                        .and_then(|d| d.as_str())
240                        .unwrap_or("")
241                        .to_string();
242                    let parameters = function
243                        .get("parameters")
244                        .cloned()
245                        .unwrap_or_else(|| json!({}));
246                    Some(ToolDefinition::function(
247                        name.to_string(),
248                        description,
249                        parameters,
250                    ))
251                })
252                .collect();
253
254            if converted.is_empty() {
255                None
256            } else {
257                Some(converted)
258            }
259        });
260        let temperature = value
261            .get("temperature")
262            .and_then(|v| v.as_f64())
263            .map(|v| v as f32);
264        let max_tokens = value
265            .get("max_tokens")
266            .and_then(|v| v.as_u64())
267            .map(|v| v as u32);
268        let stream = value
269            .get("stream")
270            .and_then(|v| v.as_bool())
271            .unwrap_or(false);
272        let tool_choice = value.get("tool_choice").and_then(Self::parse_tool_choice);
273        let parallel_tool_calls = value.get("parallel_tool_calls").and_then(|v| v.as_bool());
274        let reasoning_effort = value
275            .get("reasoning_effort")
276            .and_then(|v| v.as_str())
277            .map(|s| s.to_string())
278            .or_else(|| {
279                value
280                    .get("reasoning")
281                    .and_then(|r| r.get("effort"))
282                    .and_then(|effort| effort.as_str())
283                    .map(|s| s.to_string())
284            });
285
286        let model = value
287            .get("model")
288            .and_then(|m| m.as_str())
289            .unwrap_or(&self.model)
290            .to_string();
291
292        Some(LLMRequest {
293            messages,
294            system_prompt,
295            tools,
296            model,
297            max_tokens,
298            temperature,
299            stream,
300            tool_choice,
301            parallel_tool_calls,
302            parallel_tool_config: None,
303            reasoning_effort,
304        })
305    }
306
307    fn extract_content_text(content: &Value) -> String {
308        match content {
309            Value::String(text) => text.to_string(),
310            Value::Array(parts) => parts
311                .iter()
312                .filter_map(|part| {
313                    if let Some(text) = part.get("text").and_then(|t| t.as_str()) {
314                        Some(text.to_string())
315                    } else if let Some(Value::String(text)) = part.get("content") {
316                        Some(text.clone())
317                    } else {
318                        None
319                    }
320                })
321                .collect::<Vec<_>>()
322                .join(""),
323            _ => String::new(),
324        }
325    }
326
327    fn parse_tool_choice(choice: &Value) -> Option<ToolChoice> {
328        match choice {
329            Value::String(value) => match value.as_str() {
330                "auto" => Some(ToolChoice::auto()),
331                "none" => Some(ToolChoice::none()),
332                "required" => Some(ToolChoice::any()),
333                _ => None,
334            },
335            Value::Object(map) => {
336                let choice_type = map.get("type").and_then(|t| t.as_str())?;
337                match choice_type {
338                    "function" => map
339                        .get("function")
340                        .and_then(|f| f.get("name"))
341                        .and_then(|n| n.as_str())
342                        .map(|name| ToolChoice::function(name.to_string())),
343                    "auto" => Some(ToolChoice::auto()),
344                    "none" => Some(ToolChoice::none()),
345                    "any" | "required" => Some(ToolChoice::any()),
346                    _ => None,
347                }
348            }
349            _ => None,
350        }
351    }
352
353    fn convert_to_openai_format(&self, request: &LLMRequest) -> Result<Value, LLMError> {
354        let mut messages = Vec::new();
355
356        if let Some(system_prompt) = &request.system_prompt {
357            messages.push(json!({
358                "role": crate::config::constants::message_roles::SYSTEM,
359                "content": system_prompt
360            }));
361        }
362
363        for msg in &request.messages {
364            let role = msg.role.as_openai_str();
365            let mut message = json!({
366                "role": role,
367                "content": msg.content
368            });
369
370            if msg.role == MessageRole::Assistant {
371                if let Some(tool_calls) = &msg.tool_calls {
372                    if !tool_calls.is_empty() {
373                        let tool_calls_json: Vec<Value> = tool_calls
374                            .iter()
375                            .map(|tc| {
376                                json!({
377                                    "id": tc.id,
378                                    "type": "function",
379                                    "function": {
380                                        "name": tc.function.name,
381                                        "arguments": tc.function.arguments
382                                    }
383                                })
384                            })
385                            .collect();
386                        message["tool_calls"] = Value::Array(tool_calls_json);
387                    }
388                }
389            }
390
391            if msg.role == MessageRole::Tool {
392                if let Some(tool_call_id) = &msg.tool_call_id {
393                    message["tool_call_id"] = Value::String(tool_call_id.clone());
394                }
395            }
396
397            messages.push(message);
398        }
399
400        if messages.is_empty() {
401            let formatted_error = error_display::format_llm_error("OpenAI", "No messages provided");
402            return Err(LLMError::InvalidRequest(formatted_error));
403        }
404
405        let mut openai_request = json!({
406            "model": request.model,
407            "messages": messages,
408            "stream": request.stream
409        });
410
411        if let Some(max_tokens) = request.max_tokens {
412            if request.temperature.is_some() && Self::supports_temperature_parameter(&request.model)
413            {
414                if let Some(temperature) = request.temperature {
415                    openai_request["temperature"] = json!(temperature);
416                }
417            }
418            openai_request["max_tokens"] = json!(max_tokens);
419        }
420
421        if let Some(tools) = &request.tools {
422            if !tools.is_empty() {
423                let tools_json: Vec<Value> = tools
424                    .iter()
425                    .map(|tool| {
426                        json!({
427                            "type": "function",
428                            "name": tool.function.name,
429                            "description": tool.function.description,
430                            "parameters": tool.function.parameters
431                        })
432                    })
433                    .collect();
434                openai_request["tools"] = Value::Array(tools_json);
435            }
436        }
437
438        if let Some(tool_choice) = &request.tool_choice {
439            openai_request["tool_choice"] = tool_choice.to_provider_format("openai");
440        }
441
442        if let Some(parallel) = request.parallel_tool_calls {
443            openai_request["parallel_tool_calls"] = Value::Bool(parallel);
444        }
445
446        if let Some(effort) = request.reasoning_effort.as_deref() {
447            if self.supports_reasoning_effort(&request.model) {
448                openai_request["reasoning"] = json!({ "effort": effort });
449            }
450        }
451
452        Ok(openai_request)
453    }
454
455    fn convert_to_openai_responses_format(&self, request: &LLMRequest) -> Result<Value, LLMError> {
456        let input = if Self::is_gpt5_codex_model(&request.model) {
457            build_codex_responses_input_openai(request)?
458        } else {
459            build_standard_responses_input_openai(request)?
460        };
461
462        if input.is_empty() {
463            let formatted_error =
464                error_display::format_llm_error("OpenAI", "No messages provided for Responses API");
465            return Err(LLMError::InvalidRequest(formatted_error));
466        }
467
468        let mut openai_request = json!({
469            "model": request.model,
470            "input": input,
471            "stream": request.stream
472        });
473
474        if let Some(max_tokens) = request.max_tokens {
475            if request.temperature.is_some() && Self::supports_temperature_parameter(&request.model)
476            {
477                if let Some(temperature) = request.temperature {
478                    openai_request["temperature"] = json!(temperature);
479                }
480            }
481            openai_request["max_output_tokens"] = json!(max_tokens);
482        }
483
484        if let Some(tools) = &request.tools {
485            if !tools.is_empty() {
486                let tools_json: Vec<Value> = tools
487                    .iter()
488                    .map(|tool| {
489                        json!({
490                            "type": "function",
491                            "name": tool.function.name,
492                            "description": tool.function.description,
493                            "parameters": tool.function.parameters
494                        })
495                    })
496                    .collect();
497                openai_request["tools"] = Value::Array(tools_json);
498            }
499        }
500
501        if let Some(tool_choice) = &request.tool_choice {
502            openai_request["tool_choice"] = tool_choice.to_provider_format("openai");
503        }
504
505        if let Some(parallel) = request.parallel_tool_calls {
506            openai_request["parallel_tool_calls"] = Value::Bool(parallel);
507        }
508
509        if let Some(effort) = request.reasoning_effort.as_deref() {
510            if self.supports_reasoning_effort(&request.model) {
511                openai_request["reasoning"] = json!({ "effort": effort });
512            }
513        }
514
515        if Self::is_reasoning_model(&request.model) {
516            openai_request["reasoning"] = json!({ "effort": "medium" });
517        }
518
519        Ok(openai_request)
520    }
521
522    fn parse_openai_response(&self, response_json: Value) -> Result<LLMResponse, LLMError> {
523        let choices = response_json
524            .get("choices")
525            .and_then(|c| c.as_array())
526            .ok_or_else(|| {
527                let formatted_error = error_display::format_llm_error(
528                    "OpenAI",
529                    "Invalid response format: missing choices",
530                );
531                LLMError::Provider(formatted_error)
532            })?;
533
534        if choices.is_empty() {
535            let formatted_error =
536                error_display::format_llm_error("OpenAI", "No choices in response");
537            return Err(LLMError::Provider(formatted_error));
538        }
539
540        let choice = &choices[0];
541        let message = choice.get("message").ok_or_else(|| {
542            let formatted_error = error_display::format_llm_error(
543                "OpenAI",
544                "Invalid response format: missing message",
545            );
546            LLMError::Provider(formatted_error)
547        })?;
548
549        let content = match message.get("content") {
550            Some(Value::String(text)) => Some(text.to_string()),
551            Some(Value::Array(parts)) => {
552                let text = parts
553                    .iter()
554                    .filter_map(|part| part.get("text").and_then(|t| t.as_str()))
555                    .collect::<Vec<_>>()
556                    .join("");
557                if text.is_empty() { None } else { Some(text) }
558            }
559            _ => None,
560        };
561
562        let tool_calls = message
563            .get("tool_calls")
564            .and_then(|tc| tc.as_array())
565            .map(|calls| {
566                calls
567                    .iter()
568                    .filter_map(|call| {
569                        let id = call.get("id").and_then(|v| v.as_str())?;
570                        let function = call.get("function")?;
571                        let name = function.get("name").and_then(|v| v.as_str())?;
572                        let arguments = function.get("arguments");
573                        let serialized = arguments.map_or("{}".to_string(), |value| {
574                            if value.is_string() {
575                                value.as_str().unwrap_or("").to_string()
576                            } else {
577                                value.to_string()
578                            }
579                        });
580                        Some(ToolCall::function(
581                            id.to_string(),
582                            name.to_string(),
583                            serialized,
584                        ))
585                    })
586                    .collect::<Vec<_>>()
587            })
588            .filter(|calls| !calls.is_empty());
589
590        let reasoning = message
591            .get("reasoning")
592            .and_then(extract_reasoning_trace)
593            .or_else(|| choice.get("reasoning").and_then(extract_reasoning_trace));
594
595        let finish_reason = choice
596            .get("finish_reason")
597            .and_then(|fr| fr.as_str())
598            .map(|fr| match fr {
599                "stop" => FinishReason::Stop,
600                "length" => FinishReason::Length,
601                "tool_calls" => FinishReason::ToolCalls,
602                "content_filter" => FinishReason::ContentFilter,
603                other => FinishReason::Error(other.to_string()),
604            })
605            .unwrap_or(FinishReason::Stop);
606
607        Ok(LLMResponse {
608            content,
609            tool_calls,
610            usage: response_json.get("usage").map(|usage_value| {
611                let cached_prompt_tokens =
612                    if self.prompt_cache_enabled && self.prompt_cache_settings.surface_metrics {
613                        usage_value
614                            .get("prompt_tokens_details")
615                            .and_then(|details| details.get("cached_tokens"))
616                            .and_then(|value| value.as_u64())
617                            .map(|value| value as u32)
618                    } else {
619                        None
620                    };
621
622                crate::llm::provider::Usage {
623                    prompt_tokens: usage_value
624                        .get("prompt_tokens")
625                        .and_then(|pt| pt.as_u64())
626                        .unwrap_or(0) as u32,
627                    completion_tokens: usage_value
628                        .get("completion_tokens")
629                        .and_then(|ct| ct.as_u64())
630                        .unwrap_or(0) as u32,
631                    total_tokens: usage_value
632                        .get("total_tokens")
633                        .and_then(|tt| tt.as_u64())
634                        .unwrap_or(0) as u32,
635                    cached_prompt_tokens,
636                    cache_creation_tokens: None,
637                    cache_read_tokens: None,
638                }
639            }),
640            finish_reason,
641            reasoning,
642        })
643    }
644
645    fn parse_openai_responses_response(
646        &self,
647        response_json: Value,
648    ) -> Result<LLMResponse, LLMError> {
649        let output = response_json
650            .get("output")
651            .or_else(|| response_json.get("choices"))
652            .and_then(|value| value.as_array())
653            .ok_or_else(|| {
654                let formatted_error = error_display::format_llm_error(
655                    "OpenAI",
656                    "Invalid response format: missing output",
657                );
658                LLMError::Provider(formatted_error)
659            })?;
660
661        if output.is_empty() {
662            let formatted_error =
663                error_display::format_llm_error("OpenAI", "No output in response");
664            return Err(LLMError::Provider(formatted_error));
665        }
666
667        let mut content_fragments = Vec::new();
668        let mut reasoning_fragments = Vec::new();
669        let mut tool_calls_vec = Vec::new();
670
671        for item in output {
672            let item_type = item
673                .get("type")
674                .and_then(|value| value.as_str())
675                .unwrap_or("");
676            if item_type != "message" {
677                continue;
678            }
679
680            if let Some(content_array) = item.get("content").and_then(|value| value.as_array()) {
681                for entry in content_array {
682                    let entry_type = entry
683                        .get("type")
684                        .and_then(|value| value.as_str())
685                        .unwrap_or("");
686                    match entry_type {
687                        "output_text" | "text" => {
688                            if let Some(text) = entry.get("text").and_then(|value| value.as_str()) {
689                                if !text.is_empty() {
690                                    content_fragments.push(text.to_string());
691                                }
692                            }
693                        }
694                        "reasoning" => {
695                            if let Some(text) = entry.get("text").and_then(|value| value.as_str()) {
696                                if !text.is_empty() {
697                                    reasoning_fragments.push(text.to_string());
698                                }
699                            }
700                        }
701                        "tool_call" => {
702                            let (name_value, arguments_value) = if let Some(function) =
703                                entry.get("function").and_then(|value| value.as_object())
704                            {
705                                let name = function.get("name").and_then(|value| value.as_str());
706                                let arguments = function.get("arguments");
707                                (name, arguments)
708                            } else {
709                                let name = entry.get("name").and_then(|value| value.as_str());
710                                let arguments = entry.get("arguments");
711                                (name, arguments)
712                            };
713
714                            if let Some(name) = name_value {
715                                let id = entry
716                                    .get("id")
717                                    .and_then(|value| value.as_str())
718                                    .unwrap_or_else(|| "");
719                                let serialized =
720                                    arguments_value.map_or("{}".to_string(), |value| {
721                                        if value.is_string() {
722                                            value.as_str().unwrap_or("").to_string()
723                                        } else {
724                                            value.to_string()
725                                        }
726                                    });
727                                tool_calls_vec.push(ToolCall::function(
728                                    id.to_string(),
729                                    name.to_string(),
730                                    serialized,
731                                ));
732                            }
733                        }
734                        _ => {}
735                    }
736                }
737            }
738        }
739
740        let content = if content_fragments.is_empty() {
741            None
742        } else {
743            Some(content_fragments.join(""))
744        };
745
746        let reasoning = if reasoning_fragments.is_empty() {
747            None
748        } else {
749            Some(reasoning_fragments.join(""))
750        };
751
752        let tool_calls = if tool_calls_vec.is_empty() {
753            None
754        } else {
755            Some(tool_calls_vec)
756        };
757
758        let usage = response_json.get("usage").map(|usage_value| {
759            let cached_prompt_tokens =
760                if self.prompt_cache_enabled && self.prompt_cache_settings.surface_metrics {
761                    usage_value
762                        .get("prompt_tokens_details")
763                        .and_then(|details| details.get("cached_tokens"))
764                        .or_else(|| usage_value.get("prompt_cache_hit_tokens"))
765                        .and_then(|value| value.as_u64())
766                        .map(|value| value as u32)
767                } else {
768                    None
769                };
770
771            crate::llm::provider::Usage {
772                prompt_tokens: usage_value
773                    .get("input_tokens")
774                    .or_else(|| usage_value.get("prompt_tokens"))
775                    .and_then(|pt| pt.as_u64())
776                    .unwrap_or(0) as u32,
777                completion_tokens: usage_value
778                    .get("output_tokens")
779                    .or_else(|| usage_value.get("completion_tokens"))
780                    .and_then(|ct| ct.as_u64())
781                    .unwrap_or(0) as u32,
782                total_tokens: usage_value
783                    .get("total_tokens")
784                    .and_then(|tt| tt.as_u64())
785                    .unwrap_or(0) as u32,
786                cached_prompt_tokens,
787                cache_creation_tokens: None,
788                cache_read_tokens: None,
789            }
790        });
791
792        let stop_reason = response_json
793            .get("stop_reason")
794            .and_then(|value| value.as_str())
795            .or_else(|| {
796                output
797                    .iter()
798                    .find_map(|item| item.get("stop_reason").and_then(|value| value.as_str()))
799            })
800            .unwrap_or("stop");
801
802        let finish_reason = match stop_reason {
803            "stop" => FinishReason::Stop,
804            "max_output_tokens" | "length" => FinishReason::Length,
805            "tool_use" | "tool_calls" => FinishReason::ToolCalls,
806            other => FinishReason::Error(other.to_string()),
807        };
808
809        Ok(LLMResponse {
810            content,
811            tool_calls,
812            usage,
813            finish_reason,
814            reasoning,
815        })
816    }
817}
818
819fn build_standard_responses_input_openai(request: &LLMRequest) -> Result<Vec<Value>, LLMError> {
820    let mut input = Vec::new();
821
822    if let Some(system_prompt) = &request.system_prompt {
823        if !system_prompt.trim().is_empty() {
824            input.push(json!({
825                "role": "developer",
826                "content": [{
827                    "type": "input_text",
828                    "text": system_prompt.clone()
829                }]
830            }));
831        }
832    }
833
834    for msg in &request.messages {
835        match msg.role {
836            MessageRole::System => {
837                if !msg.content.trim().is_empty() {
838                    input.push(json!({
839                        "role": "developer",
840                        "content": [{
841                            "type": "input_text",
842                            "text": msg.content.clone()
843                        }]
844                    }));
845                }
846            }
847            MessageRole::User => {
848                input.push(json!({
849                    "role": "user",
850                    "content": [{
851                        "type": "input_text",
852                        "text": msg.content.clone()
853                    }]
854                }));
855            }
856            MessageRole::Assistant => {
857                let mut content_parts = Vec::new();
858                if !msg.content.is_empty() {
859                    content_parts.push(json!({
860                        "type": "output_text",
861                        "text": msg.content.clone()
862                    }));
863                }
864
865                if let Some(tool_calls) = &msg.tool_calls {
866                    for call in tool_calls {
867                        content_parts.push(json!({
868                            "type": "tool_call",
869                            "id": call.id.clone(),
870                            "function": {
871                                "name": call.function.name.clone(),
872                                "arguments": call.function.arguments.clone()
873                            }
874                        }));
875                    }
876                }
877
878                if !content_parts.is_empty() {
879                    input.push(json!({
880                        "role": "assistant",
881                        "content": content_parts
882                    }));
883                }
884            }
885            MessageRole::Tool => {
886                let tool_call_id = msg.tool_call_id.clone().ok_or_else(|| {
887                    let formatted_error = error_display::format_llm_error(
888                        "OpenAI",
889                        "Tool messages must include tool_call_id for Responses API",
890                    );
891                    LLMError::InvalidRequest(formatted_error)
892                })?;
893
894                let mut tool_content = Vec::new();
895                if !msg.content.trim().is_empty() {
896                    tool_content.push(json!({
897                        "type": "output_text",
898                        "text": msg.content.clone()
899                    }));
900                }
901
902                let mut tool_result = json!({
903                    "type": "tool_result",
904                    "tool_call_id": tool_call_id
905                });
906
907                if !tool_content.is_empty() {
908                    if let Value::Object(ref mut map) = tool_result {
909                        map.insert("content".to_string(), json!(tool_content));
910                    }
911                }
912
913                input.push(json!({
914                    "role": "tool",
915                    "content": [tool_result]
916                }));
917            }
918        }
919    }
920
921    Ok(input)
922}
923
924fn build_codex_responses_input_openai(request: &LLMRequest) -> Result<Vec<Value>, LLMError> {
925    let mut additional_guidance = Vec::new();
926
927    if let Some(system_prompt) = &request.system_prompt {
928        let trimmed = system_prompt.trim();
929        if !trimmed.is_empty() {
930            additional_guidance.push(trimmed.to_string());
931        }
932    }
933
934    let mut input = Vec::new();
935
936    for msg in &request.messages {
937        match msg.role {
938            MessageRole::System => {
939                let trimmed = msg.content.trim();
940                if !trimmed.is_empty() {
941                    additional_guidance.push(trimmed.to_string());
942                }
943            }
944            MessageRole::User => {
945                input.push(json!({
946                    "role": "user",
947                    "content": [{
948                        "type": "input_text",
949                        "text": msg.content.clone()
950                    }]
951                }));
952            }
953            MessageRole::Assistant => {
954                let mut content_parts = Vec::new();
955                if !msg.content.is_empty() {
956                    content_parts.push(json!({
957                        "type": "output_text",
958                        "text": msg.content.clone()
959                    }));
960                }
961
962                if let Some(tool_calls) = &msg.tool_calls {
963                    for call in tool_calls {
964                        content_parts.push(json!({
965                            "type": "tool_call",
966                            "id": call.id.clone(),
967                            "function": {
968                                "name": call.function.name.clone(),
969                                "arguments": call.function.arguments.clone()
970                            }
971                        }));
972                    }
973                }
974
975                if !content_parts.is_empty() {
976                    input.push(json!({
977                        "role": "assistant",
978                        "content": content_parts
979                    }));
980                }
981            }
982            MessageRole::Tool => {
983                let tool_call_id = msg.tool_call_id.clone().ok_or_else(|| {
984                    let formatted_error = error_display::format_llm_error(
985                        "OpenAI",
986                        "Tool messages must include tool_call_id for Responses API",
987                    );
988                    LLMError::InvalidRequest(formatted_error)
989                })?;
990
991                let mut tool_content = Vec::new();
992                if !msg.content.trim().is_empty() {
993                    tool_content.push(json!({
994                        "type": "output_text",
995                        "text": msg.content.clone()
996                    }));
997                }
998
999                let mut tool_result = json!({
1000                    "type": "tool_result",
1001                    "tool_call_id": tool_call_id
1002                });
1003
1004                if !tool_content.is_empty() {
1005                    if let Value::Object(ref mut map) = tool_result {
1006                        map.insert("content".to_string(), json!(tool_content));
1007                    }
1008                }
1009
1010                input.push(json!({
1011                    "role": "tool",
1012                    "content": [tool_result]
1013                }));
1014            }
1015        }
1016    }
1017
1018    let developer_prompt = gpt5_codex_developer_prompt(&additional_guidance);
1019    input.insert(
1020        0,
1021        json!({
1022            "role": "developer",
1023            "content": [{
1024                "type": "input_text",
1025                "text": developer_prompt
1026            }]
1027        }),
1028    );
1029
1030    Ok(input)
1031}
1032
1033#[async_trait]
1034impl LLMProvider for OpenAIProvider {
1035    fn name(&self) -> &str {
1036        "openai"
1037    }
1038
1039    fn supports_reasoning(&self, _model: &str) -> bool {
1040        false
1041    }
1042
1043    fn supports_reasoning_effort(&self, model: &str) -> bool {
1044        let requested = if model.trim().is_empty() {
1045            self.model.as_str()
1046        } else {
1047            model
1048        };
1049        models::openai::REASONING_MODELS
1050            .iter()
1051            .any(|candidate| *candidate == requested)
1052    }
1053
1054    async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
1055        let mut request = request;
1056        if request.model.trim().is_empty() {
1057            request.model = self.model.clone();
1058        }
1059
1060        if Self::uses_responses_api(&request.model) {
1061            let openai_request = self.convert_to_openai_responses_format(&request)?;
1062            let url = format!("{}/responses", self.base_url);
1063
1064            let response = self
1065                .http_client
1066                .post(&url)
1067                .bearer_auth(&self.api_key)
1068                .json(&openai_request)
1069                .send()
1070                .await
1071                .map_err(|e| {
1072                    let formatted_error =
1073                        error_display::format_llm_error("OpenAI", &format!("Network error: {}", e));
1074                    LLMError::Network(formatted_error)
1075                })?;
1076
1077            if !response.status().is_success() {
1078                let status = response.status();
1079                let error_text = response.text().await.unwrap_or_default();
1080
1081                if status.as_u16() == 429
1082                    || error_text.contains("insufficient_quota")
1083                    || error_text.contains("quota")
1084                    || error_text.contains("rate limit")
1085                {
1086                    return Err(LLMError::RateLimit);
1087                }
1088
1089                let formatted_error = error_display::format_llm_error(
1090                    "OpenAI",
1091                    &format!("HTTP {}: {}", status, error_text),
1092                );
1093                return Err(LLMError::Provider(formatted_error));
1094            }
1095
1096            let openai_response: Value = response.json().await.map_err(|e| {
1097                let formatted_error = error_display::format_llm_error(
1098                    "OpenAI",
1099                    &format!("Failed to parse response: {}", e),
1100                );
1101                LLMError::Provider(formatted_error)
1102            })?;
1103
1104            self.parse_openai_responses_response(openai_response)
1105        } else {
1106            let openai_request = self.convert_to_openai_format(&request)?;
1107            let url = format!("{}/chat/completions", self.base_url);
1108
1109            let response = self
1110                .http_client
1111                .post(&url)
1112                .bearer_auth(&self.api_key)
1113                .json(&openai_request)
1114                .send()
1115                .await
1116                .map_err(|e| {
1117                    let formatted_error =
1118                        error_display::format_llm_error("OpenAI", &format!("Network error: {}", e));
1119                    LLMError::Network(formatted_error)
1120                })?;
1121
1122            if !response.status().is_success() {
1123                let status = response.status();
1124                let error_text = response.text().await.unwrap_or_default();
1125
1126                if status.as_u16() == 429
1127                    || error_text.contains("insufficient_quota")
1128                    || error_text.contains("quota")
1129                    || error_text.contains("rate limit")
1130                {
1131                    return Err(LLMError::RateLimit);
1132                }
1133
1134                let formatted_error = error_display::format_llm_error(
1135                    "OpenAI",
1136                    &format!("HTTP {}: {}", status, error_text),
1137                );
1138                return Err(LLMError::Provider(formatted_error));
1139            }
1140
1141            let openai_response: Value = response.json().await.map_err(|e| {
1142                let formatted_error = error_display::format_llm_error(
1143                    "OpenAI",
1144                    &format!("Failed to parse response: {}", e),
1145                );
1146                LLMError::Provider(formatted_error)
1147            })?;
1148
1149            self.parse_openai_response(openai_response)
1150        }
1151    }
1152
1153    fn supported_models(&self) -> Vec<String> {
1154        models::openai::SUPPORTED_MODELS
1155            .iter()
1156            .map(|s| s.to_string())
1157            .collect()
1158    }
1159
1160    fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
1161        if request.messages.is_empty() {
1162            let formatted_error =
1163                error_display::format_llm_error("OpenAI", "Messages cannot be empty");
1164            return Err(LLMError::InvalidRequest(formatted_error));
1165        }
1166
1167        if !self.supported_models().contains(&request.model) {
1168            let formatted_error = error_display::format_llm_error(
1169                "OpenAI",
1170                &format!("Unsupported model: {}", request.model),
1171            );
1172            return Err(LLMError::InvalidRequest(formatted_error));
1173        }
1174
1175        for message in &request.messages {
1176            if let Err(err) = message.validate_for_provider("openai") {
1177                let formatted = error_display::format_llm_error("OpenAI", &err);
1178                return Err(LLMError::InvalidRequest(formatted));
1179            }
1180        }
1181
1182        Ok(())
1183    }
1184}
1185
1186#[async_trait]
1187impl LLMClient for OpenAIProvider {
1188    async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, LLMError> {
1189        let request = self.parse_client_prompt(prompt);
1190        let request_model = request.model.clone();
1191        let response = LLMProvider::generate(self, request).await?;
1192
1193        Ok(llm_types::LLMResponse {
1194            content: response.content.unwrap_or_default(),
1195            model: request_model,
1196            usage: response.usage.map(|u| llm_types::Usage {
1197                prompt_tokens: u.prompt_tokens as usize,
1198                completion_tokens: u.completion_tokens as usize,
1199                total_tokens: u.total_tokens as usize,
1200                cached_prompt_tokens: u.cached_prompt_tokens.map(|v| v as usize),
1201                cache_creation_tokens: u.cache_creation_tokens.map(|v| v as usize),
1202                cache_read_tokens: u.cache_read_tokens.map(|v| v as usize),
1203            }),
1204            reasoning: response.reasoning,
1205        })
1206    }
1207
1208    fn backend_kind(&self) -> llm_types::BackendKind {
1209        llm_types::BackendKind::OpenAI
1210    }
1211
1212    fn model_id(&self) -> &str {
1213        &self.model
1214    }
1215}