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 mut request: provider::LLMRequest,
175 ) -> Result<provider::LLMResponse, provider::LLMError> {
176 if self.prompt_cache_settings.auxiliary_explicit_cache {
179 request
180 .prompt_cache_profile
181 .get_or_insert(provider::PromptCacheProfile::StablePrefix);
182 }
183 let started = std::time::Instant::now();
184 let requested_cache_profile = request.prompt_cache_profile;
185 let response = self.generate_request_with_websocket(request, false).await?;
186 if self.is_native_openai_api()
187 && let Some(usage) = response.usage.as_ref()
188 {
189 tracing::debug!(
190 target: "vtcode::llm::prompt_cache",
191 kind = "prompt_cache_metrics",
192 scope = "auxiliary",
193 provider = "openai",
194 model = %response.model,
195 requested_cache_profile = ?requested_cache_profile,
196 input_tokens = usage.prompt_tokens,
197 cache_read_tokens_reported = usage.cache_read_tokens.or(usage.cached_prompt_tokens),
198 cache_write_tokens_reported = usage.cache_creation_tokens,
199 elapsed_ms = started.elapsed().as_millis(),
200 "OpenAI auxiliary cache usage"
201 );
202 }
203 Ok(response)
204 }
205
206 async fn compact_history(
207 &self,
208 model: &str,
209 history: &[provider::Message],
210 ) -> Result<Vec<provider::Message>, provider::LLMError> {
211 if !self.supports_manual_openai_compaction(model) {
212 return Err(provider::LLMError::Provider {
213 message: "OpenAI Responses compaction is not supported for this endpoint/model".to_string(),
214 metadata: None,
215 });
216 }
217
218 self.compact_history_request(model, history).await
219 }
220
221 async fn compact_history_with_options(
222 &self,
223 model: &str,
224 history: &[provider::Message],
225 options: &provider::ResponsesCompactionOptions,
226 ) -> Result<Vec<provider::Message>, provider::LLMError> {
227 let requested = if model.trim().is_empty() {
228 self.model.as_ref()
229 } else {
230 model
231 };
232 if !self.supports_manual_openai_compaction_for_model(requested) {
233 return Err(provider::LLMError::Provider {
234 message: self.manual_openai_compaction_unavailable_message_for_model(requested),
235 metadata: None,
236 });
237 }
238
239 self.compact_history_request_with_options(requested, history, options).await
240 }
241
242 fn supported_models(&self) -> Vec<String> {
243 if let Some(models) = &self.supported_models_override {
244 return models.clone();
245 }
246 if self.provider_key_override.is_some() {
247 return vec![self.model.to_string()];
248 }
249 models::openai::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
250 }
251
252 fn validate_request(&self, request: &provider::LLMRequest) -> Result<(), provider::LLMError> {
253 let supported_models = (!self.is_native_openai_api()).then(|| self.supported_models());
254
255 let display_name = self.provider_display_override.as_deref().unwrap_or("OpenAI");
256 let key = self.provider_key_override.as_deref().unwrap_or("openai");
257 super::super::super::common::validate_request_common(request, display_name, key, supported_models.as_deref())
258 }
259}
260
261#[async_trait]
262impl LLMClient for OpenAIProvider {
263 async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, provider::LLMError> {
264 let request = super::super::super::common::make_default_request(prompt, &self.model);
265 let request_model = request.model.to_string();
266 let response = provider::LLMProvider::generate(self, request).await?;
267
268 Ok(llm_types::LLMResponse {
269 content: Some(response.content.unwrap_or_default()),
270 model: request_model,
271 usage: response.usage.map(super::super::super::common::convert_usage_to_llm_types),
272 reasoning: response.reasoning,
273 reasoning_details: response.reasoning_details,
274 request_id: response.request_id,
275 organization_id: response.organization_id,
276 finish_reason: response.finish_reason,
277 tool_calls: response.tool_calls,
278 tool_references: response.tool_references,
279 compaction: None,
280 })
281 }
282
283 fn model_id(&self) -> &str {
284 &self.model
285 }
286}