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