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 requested_prompt_cache_ttl(&self, model: &str) -> Option<vtcode_config::core::prompt_cache::PromptCacheTtl> {
100        (self.prompt_cache_hints_enabled
101            && !self.is_chatgpt_backend()
102            && !matches!(self.responses_api_state(model), super::super::types::ResponsesApiState::Disabled)
103            && ((self.is_native_openai_api() && super::super::request_builder::supports_prompt_cache_options(model))
104                || self.prompt_cache_options_models.iter().any(|configured| configured == model)))
105        .then(|| {
106            vtcode_config::core::prompt_cache::resolved_openai_prompt_cache_ttl(
107                self.prompt_cache_settings.prompt_cache_ttl,
108                model,
109            )
110        })
111    }
112
113    fn supports_responses_compaction(&self, model: &str) -> bool {
114        if self.is_chatgpt_backend() {
115            return false;
116        }
117        let requested = if model.trim().is_empty() {
118            self.model.as_ref()
119        } else {
120            model
121        };
122        !matches!(self.responses_api_state(requested), super::super::types::ResponsesApiState::Disabled)
123    }
124
125    fn supports_native_allowed_tools(&self, model: &str) -> bool {
126        self.supports_responses_allowed_tools(model)
127    }
128
129    fn supports_manual_openai_compaction(&self, model: &str) -> bool {
130        let requested = if model.trim().is_empty() {
131            self.model.as_ref()
132        } else {
133            model
134        };
135        self.supports_manual_openai_compaction_for_model(requested)
136    }
137
138    fn manual_openai_compaction_unavailable_message(&self, model: &str) -> String {
139        self.manual_openai_compaction_unavailable_message_for_model(model)
140    }
141
142    async fn stream(&self, request: provider::LLMRequest) -> Result<provider::LLMStream, provider::LLMError> {
143        let Some(tracker) = self.cache_comparison_tracker(&request) else {
144            return self.stream_request(request).await;
145        };
146        let mut stream = self.stream_request(request).await?;
147        Ok(Box::pin(async_stream::try_stream! {
148            while let Some(event) = stream.next().await {
149                let event = event?;
150                if let provider::LLMStreamEvent::Completed { response } = &event {
151                    tracker.record_completion(response);
152                }
153                yield event;
154            }
155        }))
156    }
157
158    async fn stream_normalized(
159        &self,
160        request: provider::LLMRequest,
161    ) -> Result<LLMNormalizedStream, provider::LLMError> {
162        let Some(tracker) = self.cache_comparison_tracker(&request) else {
163            return self.stream_normalized_request(request).await;
164        };
165        let mut stream = self.stream_normalized_request(request).await?;
166        Ok(Box::pin(async_stream::try_stream! {
167            while let Some(event) = stream.next().await {
168                let event = event?;
169                if let provider::NormalizedStreamEvent::Done { response } = &event {
170                    tracker.record_completion(response);
171                }
172                yield event;
173            }
174        }))
175    }
176
177    async fn generate(&self, request: provider::LLMRequest) -> Result<provider::LLMResponse, provider::LLMError> {
178        let tracker = self.cache_comparison_tracker(&request);
179        let response = self.generate_request(request).await?;
180        if let Some(tracker) = tracker {
181            tracker.record_completion(&response);
182        }
183        Ok(response)
184    }
185
186    async fn generate_auxiliary(
187        &self,
188        mut request: provider::LLMRequest,
189    ) -> Result<provider::LLMResponse, provider::LLMError> {
190        // Auxiliary output is not replayed into this request's conversation.
191        // Avoid writing its disposable suffix while retaining stable instructions.
192        if self.prompt_cache_settings.auxiliary_explicit_cache {
193            request
194                .prompt_cache_profile
195                .get_or_insert(provider::PromptCacheProfile::StablePrefix);
196        }
197        let started = std::time::Instant::now();
198        let requested_cache_profile = request.prompt_cache_profile;
199        let response = self.generate_request_with_websocket(request, false).await?;
200        if self.is_native_openai_api()
201            && let Some(usage) = response.usage.as_ref()
202        {
203            tracing::debug!(
204                target: "vtcode::llm::prompt_cache",
205                kind = "prompt_cache_metrics",
206                scope = "auxiliary",
207                provider = "openai",
208                model = %response.model,
209                requested_cache_profile = ?requested_cache_profile,
210                input_tokens = usage.prompt_tokens,
211                cache_read_tokens_reported = usage.cache_read_tokens.or(usage.cached_prompt_tokens),
212                cache_write_tokens_reported = usage.cache_creation_tokens,
213                elapsed_ms = started.elapsed().as_millis(),
214                "OpenAI auxiliary cache usage"
215            );
216        }
217        Ok(response)
218    }
219
220    async fn compact_history(
221        &self,
222        model: &str,
223        history: &[provider::Message],
224    ) -> Result<Vec<provider::Message>, provider::LLMError> {
225        if !self.supports_manual_openai_compaction(model) {
226            return Err(provider::LLMError::Provider {
227                message: "OpenAI Responses compaction is not supported for this endpoint/model".to_string(),
228                metadata: None,
229            });
230        }
231
232        self.compact_history_request(model, history).await
233    }
234
235    async fn compact_history_with_options(
236        &self,
237        model: &str,
238        history: &[provider::Message],
239        options: &provider::ResponsesCompactionOptions,
240    ) -> Result<Vec<provider::Message>, provider::LLMError> {
241        let requested = if model.trim().is_empty() {
242            self.model.as_ref()
243        } else {
244            model
245        };
246        if !self.supports_manual_openai_compaction_for_model(requested) {
247            return Err(provider::LLMError::Provider {
248                message: self.manual_openai_compaction_unavailable_message_for_model(requested),
249                metadata: None,
250            });
251        }
252
253        self.compact_history_request_with_options(requested, history, options).await
254    }
255
256    fn supported_models(&self) -> Vec<String> {
257        if let Some(models) = &self.supported_models_override {
258            return models.clone();
259        }
260        if self.provider_key_override.is_some() {
261            return vec![self.model.to_string()];
262        }
263        models::openai::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
264    }
265
266    fn validate_request(&self, request: &provider::LLMRequest) -> Result<(), provider::LLMError> {
267        let supported_models = (!self.is_native_openai_api()).then(|| self.supported_models());
268
269        let display_name = self.provider_display_override.as_deref().unwrap_or("OpenAI");
270        let key = self.provider_key_override.as_deref().unwrap_or("openai");
271        super::super::super::common::validate_request_common(request, display_name, key, supported_models.as_deref())
272    }
273}
274
275#[async_trait]
276impl LLMClient for OpenAIProvider {
277    async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, provider::LLMError> {
278        let request = super::super::super::common::make_default_request(prompt, &self.model);
279        let request_model = request.model.to_string();
280        let response = provider::LLMProvider::generate(self, request).await?;
281
282        Ok(llm_types::LLMResponse {
283            content: Some(response.content.unwrap_or_default()),
284            model: request_model,
285            usage: response.usage.map(super::super::super::common::convert_usage_to_llm_types),
286            reasoning: response.reasoning,
287            reasoning_details: response.reasoning_details,
288            request_id: response.request_id,
289            organization_id: response.organization_id,
290            finish_reason: response.finish_reason,
291            tool_calls: response.tool_calls,
292            tool_references: response.tool_references,
293            compaction: None,
294        })
295    }
296
297    fn model_id(&self) -> &str {
298        &self.model
299    }
300}