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 compact_history(
143        &self,
144        model: &str,
145        history: &[provider::Message],
146    ) -> Result<Vec<provider::Message>, provider::LLMError> {
147        if !self.supports_manual_openai_compaction(model) {
148            return Err(provider::LLMError::Provider {
149                message: "OpenAI Responses compaction is not supported for this endpoint/model".to_string(),
150                metadata: None,
151            });
152        }
153
154        self.compact_history_request(model, history).await
155    }
156
157    async fn compact_history_with_options(
158        &self,
159        model: &str,
160        history: &[provider::Message],
161        options: &provider::ResponsesCompactionOptions,
162    ) -> Result<Vec<provider::Message>, provider::LLMError> {
163        let requested = if model.trim().is_empty() {
164            self.model.as_ref()
165        } else {
166            model
167        };
168        if !self.supports_manual_openai_compaction_for_model(requested) {
169            return Err(provider::LLMError::Provider {
170                message: self.manual_openai_compaction_unavailable_message_for_model(requested),
171                metadata: None,
172            });
173        }
174
175        self.compact_history_request_with_options(requested, history, options).await
176    }
177
178    fn supported_models(&self) -> Vec<String> {
179        if let Some(models) = &self.supported_models_override {
180            return models.clone();
181        }
182        if self.provider_key_override.is_some() {
183            return vec![self.model.to_string()];
184        }
185        models::openai::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
186    }
187
188    fn validate_request(&self, request: &provider::LLMRequest) -> Result<(), provider::LLMError> {
189        let supported_models = (!self.is_native_openai_api()).then(|| self.supported_models());
190
191        let display_name = self.provider_display_override.as_deref().unwrap_or("OpenAI");
192        let key = self.provider_key_override.as_deref().unwrap_or("openai");
193        super::super::super::common::validate_request_common(request, display_name, key, supported_models.as_deref())
194    }
195}
196
197#[async_trait]
198impl LLMClient for OpenAIProvider {
199    async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, provider::LLMError> {
200        let request = super::super::super::common::make_default_request(prompt, &self.model);
201        let request_model = request.model.to_string();
202        let response = provider::LLMProvider::generate(self, request).await?;
203
204        Ok(llm_types::LLMResponse {
205            content: Some(response.content.unwrap_or_default()),
206            model: request_model,
207            usage: response.usage.map(super::super::super::common::convert_usage_to_llm_types),
208            reasoning: response.reasoning,
209            reasoning_details: response.reasoning_details,
210            request_id: response.request_id,
211            organization_id: response.organization_id,
212            finish_reason: response.finish_reason,
213            tool_calls: response.tool_calls,
214            tool_references: response.tool_references,
215            compaction: None,
216        })
217    }
218
219    fn model_id(&self) -> &str {
220        &self.model
221    }
222}