vtcode_llm/providers/openai/provider/
provider_impl.rs1use 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 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 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}