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