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 requested_prompt_cache_ttl(&self, model: &str) -> Option<vtcode_config::core::prompt_cache::PromptCacheTtl> {
100 (self.prompt_cache_hints_enabled
101 && !self.is_chatgpt_backend()
102 && !matches!(self.responses_api_state(model), super::super::types::ResponsesApiState::Disabled)
103 && ((self.is_native_openai_api() && super::super::request_builder::supports_prompt_cache_options(model))
104 || self.prompt_cache_options_models.iter().any(|configured| configured == model)))
105 .then(|| {
106 vtcode_config::core::prompt_cache::resolved_openai_prompt_cache_ttl(
107 self.prompt_cache_settings.prompt_cache_ttl,
108 model,
109 )
110 })
111 }
112
113 fn supports_responses_compaction(&self, model: &str) -> bool {
114 if self.is_chatgpt_backend() {
115 return false;
116 }
117 let requested = if model.trim().is_empty() {
118 self.model.as_ref()
119 } else {
120 model
121 };
122 !matches!(self.responses_api_state(requested), super::super::types::ResponsesApiState::Disabled)
123 }
124
125 fn supports_native_allowed_tools(&self, model: &str) -> bool {
126 self.supports_responses_allowed_tools(model)
127 }
128
129 fn supports_manual_openai_compaction(&self, model: &str) -> bool {
130 let requested = if model.trim().is_empty() {
131 self.model.as_ref()
132 } else {
133 model
134 };
135 self.supports_manual_openai_compaction_for_model(requested)
136 }
137
138 fn manual_openai_compaction_unavailable_message(&self, model: &str) -> String {
139 self.manual_openai_compaction_unavailable_message_for_model(model)
140 }
141
142 async fn stream(&self, request: provider::LLMRequest) -> Result<provider::LLMStream, provider::LLMError> {
143 let Some(tracker) = self.cache_comparison_tracker(&request) else {
144 return self.stream_request(request).await;
145 };
146 let mut stream = self.stream_request(request).await?;
147 Ok(Box::pin(async_stream::try_stream! {
148 while let Some(event) = stream.next().await {
149 let event = event?;
150 if let provider::LLMStreamEvent::Completed { response } = &event {
151 tracker.record_completion(response);
152 }
153 yield event;
154 }
155 }))
156 }
157
158 async fn stream_normalized(
159 &self,
160 request: provider::LLMRequest,
161 ) -> Result<LLMNormalizedStream, provider::LLMError> {
162 let Some(tracker) = self.cache_comparison_tracker(&request) else {
163 return self.stream_normalized_request(request).await;
164 };
165 let mut stream = self.stream_normalized_request(request).await?;
166 Ok(Box::pin(async_stream::try_stream! {
167 while let Some(event) = stream.next().await {
168 let event = event?;
169 if let provider::NormalizedStreamEvent::Done { response } = &event {
170 tracker.record_completion(response);
171 }
172 yield event;
173 }
174 }))
175 }
176
177 async fn generate(&self, request: provider::LLMRequest) -> Result<provider::LLMResponse, provider::LLMError> {
178 let tracker = self.cache_comparison_tracker(&request);
179 let response = self.generate_request(request).await?;
180 if let Some(tracker) = tracker {
181 tracker.record_completion(&response);
182 }
183 Ok(response)
184 }
185
186 async fn generate_auxiliary(
187 &self,
188 mut request: provider::LLMRequest,
189 ) -> Result<provider::LLMResponse, provider::LLMError> {
190 if self.prompt_cache_settings.auxiliary_explicit_cache {
193 request
194 .prompt_cache_profile
195 .get_or_insert(provider::PromptCacheProfile::StablePrefix);
196 }
197 let started = std::time::Instant::now();
198 let requested_cache_profile = request.prompt_cache_profile;
199 let response = self.generate_request_with_websocket(request, false).await?;
200 if self.is_native_openai_api()
201 && let Some(usage) = response.usage.as_ref()
202 {
203 tracing::debug!(
204 target: "vtcode::llm::prompt_cache",
205 kind = "prompt_cache_metrics",
206 scope = "auxiliary",
207 provider = "openai",
208 model = %response.model,
209 requested_cache_profile = ?requested_cache_profile,
210 input_tokens = usage.prompt_tokens,
211 cache_read_tokens_reported = usage.cache_read_tokens.or(usage.cached_prompt_tokens),
212 cache_write_tokens_reported = usage.cache_creation_tokens,
213 elapsed_ms = started.elapsed().as_millis(),
214 "OpenAI auxiliary cache usage"
215 );
216 }
217 Ok(response)
218 }
219
220 async fn compact_history(
221 &self,
222 model: &str,
223 history: &[provider::Message],
224 ) -> Result<Vec<provider::Message>, provider::LLMError> {
225 if !self.supports_manual_openai_compaction(model) {
226 return Err(provider::LLMError::Provider {
227 message: "OpenAI Responses compaction is not supported for this endpoint/model".to_string(),
228 metadata: None,
229 });
230 }
231
232 self.compact_history_request(model, history).await
233 }
234
235 async fn compact_history_with_options(
236 &self,
237 model: &str,
238 history: &[provider::Message],
239 options: &provider::ResponsesCompactionOptions,
240 ) -> Result<Vec<provider::Message>, provider::LLMError> {
241 let requested = if model.trim().is_empty() {
242 self.model.as_ref()
243 } else {
244 model
245 };
246 if !self.supports_manual_openai_compaction_for_model(requested) {
247 return Err(provider::LLMError::Provider {
248 message: self.manual_openai_compaction_unavailable_message_for_model(requested),
249 metadata: None,
250 });
251 }
252
253 self.compact_history_request_with_options(requested, history, options).await
254 }
255
256 fn supported_models(&self) -> Vec<String> {
257 if let Some(models) = &self.supported_models_override {
258 return models.clone();
259 }
260 if self.provider_key_override.is_some() {
261 return vec![self.model.to_string()];
262 }
263 models::openai::SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
264 }
265
266 fn validate_request(&self, request: &provider::LLMRequest) -> Result<(), provider::LLMError> {
267 let supported_models = (!self.is_native_openai_api()).then(|| self.supported_models());
268
269 let display_name = self.provider_display_override.as_deref().unwrap_or("OpenAI");
270 let key = self.provider_key_override.as_deref().unwrap_or("openai");
271 super::super::super::common::validate_request_common(request, display_name, key, supported_models.as_deref())
272 }
273}
274
275#[async_trait]
276impl LLMClient for OpenAIProvider {
277 async fn generate(&mut self, prompt: &str) -> Result<llm_types::LLMResponse, provider::LLMError> {
278 let request = super::super::super::common::make_default_request(prompt, &self.model);
279 let request_model = request.model.to_string();
280 let response = provider::LLMProvider::generate(self, request).await?;
281
282 Ok(llm_types::LLMResponse {
283 content: Some(response.content.unwrap_or_default()),
284 model: request_model,
285 usage: response.usage.map(super::super::super::common::convert_usage_to_llm_types),
286 reasoning: response.reasoning,
287 reasoning_details: response.reasoning_details,
288 request_id: response.request_id,
289 organization_id: response.organization_id,
290 finish_reason: response.finish_reason,
291 tool_calls: response.tool_calls,
292 tool_references: response.tool_references,
293 compaction: None,
294 })
295 }
296
297 fn model_id(&self) -> &str {
298 &self.model
299 }
300}