Skip to main content

vtcode_llm/providers/openai/provider/
provider_impl.rs

1use super::OpenAIProvider;
2use crate::client::LLMClient;
3use crate::provider::{self, LLMNormalizedStream};
4use crate::types as llm_types;
5use async_trait::async_trait;
6use futures::StreamExt;
7use vtcode_config::constants::models;
8
9#[async_trait]
10impl provider::LLMProvider for OpenAIProvider {
11    fn supports_decisions(&self) -> bool {
12        self.decisions_eligible()
13    }
14
15    async fn decide_choice(
16        &self,
17        request: provider::ChoiceDecisionRequest,
18    ) -> Result<provider::ChoiceDecisionResponse, provider::LLMError> {
19        self.decide_choice_request(request).await
20    }
21    fn name(&self) -> &str {
22        self.provider_key_override.as_deref().unwrap_or("openai")
23    }
24
25    fn supports_streaming(&self) -> bool {
26        true
27    }
28
29    fn supports_non_streaming(&self, model: &str) -> bool {
30        let requested = if model.trim().is_empty() {
31            self.model.as_ref()
32        } else {
33            model
34        };
35
36        !self.is_chatgpt_backend() && !Self::requires_streaming_responses(requested)
37    }
38
39    fn effective_context_size(&self, model: &str) -> usize {
40        if let Some(context_window) = self.context_window_override {
41            return context_window;
42        }
43
44        let requested = if model.trim().is_empty() {
45            self.model.as_ref()
46        } else {
47            model
48        };
49
50        vtcode_config::models::model_catalog_entry(self.name(), requested)
51            .map(|entry| entry.context_window)
52            .filter(|context_window| *context_window > 0)
53            .unwrap_or(128_000)
54    }
55
56    fn supports_reasoning(&self, model: &str) -> bool {
57        let requested = if model.trim().is_empty() {
58            self.model.as_ref()
59        } else {
60            model
61        };
62
63        // Codex-inspired robustness: Setting model_supports_reasoning to false
64        // does NOT disable it for known reasoning models.
65        models::openai::REASONING_MODELS.contains(&requested)
66            || self
67                .model_behavior
68                .as_ref()
69                .and_then(|b| b.model_supports_reasoning)
70                .unwrap_or(false)
71    }
72
73    fn supports_reasoning_effort(&self, model: &str) -> bool {
74        let requested = if model.trim().is_empty() {
75            self.model.as_ref()
76        } else {
77            model
78        };
79
80        // Same robustness logic for reasoning effort
81        models::openai::REASONING_MODELS.iter().any(|candidate| *candidate == requested)
82            || self
83                .model_behavior
84                .as_ref()
85                .and_then(|b| b.model_supports_reasoning_effort)
86                .unwrap_or(false)
87    }
88
89    fn supports_tools(&self, model: &str) -> bool {
90        let requested = if model.trim().is_empty() {
91            self.model.as_ref()
92        } else {
93            model
94        };
95
96        !models::openai::TOOL_UNAVAILABLE_MODELS.contains(&requested)
97    }
98
99    fn supports_responses_compaction(&self, model: &str) -> bool {
100        if self.is_chatgpt_backend() {
101            return false;
102        }
103        let requested = if model.trim().is_empty() {
104            self.model.as_ref()
105        } else {
106            model
107        };
108        !matches!(self.responses_api_state(requested), super::super::types::ResponsesApiState::Disabled)
109    }
110
111    fn supports_native_allowed_tools(&self, model: &str) -> bool {
112        self.supports_responses_allowed_tools(model)
113    }
114
115    fn supports_manual_openai_compaction(&self, model: &str) -> bool {
116        let requested = if model.trim().is_empty() {
117            self.model.as_ref()
118        } else {
119            model
120        };
121        self.supports_manual_openai_compaction_for_model(requested)
122    }
123
124    fn manual_openai_compaction_unavailable_message(&self, model: &str) -> String {
125        self.manual_openai_compaction_unavailable_message_for_model(model)
126    }
127
128    async fn stream(&self, request: provider::LLMRequest) -> Result<provider::LLMStream, provider::LLMError> {
129        let Some(tracker) = self.cache_comparison_tracker(&request) else {
130            return self.stream_request(request).await;
131        };
132        let mut stream = self.stream_request(request).await?;
133        Ok(Box::pin(async_stream::try_stream! {
134            while let Some(event) = stream.next().await {
135                let event = event?;
136                if let provider::LLMStreamEvent::Completed { response } = &event {
137                    tracker.record_completion(response);
138                }
139                yield event;
140            }
141        }))
142    }
143
144    async fn stream_normalized(
145        &self,
146        request: provider::LLMRequest,
147    ) -> Result<LLMNormalizedStream, provider::LLMError> {
148        let Some(tracker) = self.cache_comparison_tracker(&request) else {
149            return self.stream_normalized_request(request).await;
150        };
151        let mut stream = self.stream_normalized_request(request).await?;
152        Ok(Box::pin(async_stream::try_stream! {
153            while let Some(event) = stream.next().await {
154                let event = event?;
155                if let provider::NormalizedStreamEvent::Done { response } = &event {
156                    tracker.record_completion(response);
157                }
158                yield event;
159            }
160        }))
161    }
162
163    async fn generate(&self, request: provider::LLMRequest) -> Result<provider::LLMResponse, provider::LLMError> {
164        let tracker = self.cache_comparison_tracker(&request);
165        let response = self.generate_request(request).await?;
166        if let Some(tracker) = tracker {
167            tracker.record_completion(&response);
168        }
169        Ok(response)
170    }
171
172    async fn generate_auxiliary(
173        &self,
174        request: provider::LLMRequest,
175    ) -> Result<provider::LLMResponse, provider::LLMError> {
176        self.generate_request_with_websocket(request, false).await
177    }
178
179    async fn compact_history(
180        &self,
181        model: &str,
182        history: &[provider::Message],
183    ) -> Result<Vec<provider::Message>, provider::LLMError> {
184        if !self.supports_manual_openai_compaction(model) {
185            return Err(provider::LLMError::Provider {
186                message: "OpenAI Responses compaction is not supported for this endpoint/model".to_string(),
187                metadata: None,
188            });
189        }
190
191        self.compact_history_request(model, history).await
192    }
193
194    async fn compact_history_with_options(
195        &self,
196        model: &str,
197        history: &[provider::Message],
198        options: &provider::ResponsesCompactionOptions,
199    ) -> Result<Vec<provider::Message>, provider::LLMError> {
200        let requested = if model.trim().is_empty() {
201            self.model.as_ref()
202        } else {
203            model
204        };
205        if !self.supports_manual_openai_compaction_for_model(requested) {
206            return Err(provider::LLMError::Provider {
207                message: self.manual_openai_compaction_unavailable_message_for_model(requested),
208                metadata: None,
209            });
210        }
211
212        self.compact_history_request_with_options(requested, history, options).await
213    }
214
215    fn supported_models(&self) -> Vec<String> {
216        if let Some(models) = &self.supported_models_override {
217            return models.clone();
218        }
219        if self.provider_key_override.is_some() {
220            return vec![self.model.to_string()];
221        }
222        models::openai::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
223    }
224
225    fn validate_request(&self, request: &provider::LLMRequest) -> Result<(), provider::LLMError> {
226        let supported_models = (!self.is_native_openai_api()).then(|| self.supported_models());
227
228        let display_name = self.provider_display_override.as_deref().unwrap_or("OpenAI");
229        let key = self.provider_key_override.as_deref().unwrap_or("openai");
230        super::super::super::common::validate_request_common(request, display_name, key, supported_models.as_deref())
231    }
232}
233
234#[async_trait]
235impl LLMClient for OpenAIProvider {
236    async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, provider::LLMError> {
237        let request = super::super::super::common::make_default_request(prompt, &self.model);
238        let request_model = request.model.to_string();
239        let response = provider::LLMProvider::generate(self, request).await?;
240
241        Ok(llm_types::LLMResponse {
242            content: Some(response.content.unwrap_or_default()),
243            model: request_model,
244            usage: response.usage.map(super::super::super::common::convert_usage_to_llm_types),
245            reasoning: response.reasoning,
246            reasoning_details: response.reasoning_details,
247            request_id: response.request_id,
248            organization_id: response.organization_id,
249            finish_reason: response.finish_reason,
250            tool_calls: response.tool_calls,
251            tool_references: response.tool_references,
252            compaction: None,
253        })
254    }
255
256    fn model_id(&self) -> &str {
257        &self.model
258    }
259}