vtcode_llm/providers/openresponses/provider/
provider_impl.rs1use super::OpenResponsesProvider;
4use crate::provider::{
5 LLMError, LLMNormalizedStream, LLMProvider, LLMRequest, LLMResponse, LLMStream, LLMStreamEvent, Message,
6 ResponsesCompactionOptions,
7};
8use crate::providers::error_handling::{format_network_error, format_parse_error};
9use crate::providers::shared::{
10 ResponsesNormalizedStreamOptions, Utf8StreamDecoder, create_responses_normalized_stream,
11};
12use anyhow::Result;
13use async_stream::try_stream;
14use async_trait::async_trait;
15use futures::StreamExt;
16use serde::Deserialize;
17use serde_json::{Value, json};
18
19#[derive(Debug, Deserialize)]
25struct NativeStreamEventWire<'a> {
26 #[serde(rename = "type")]
27 event_type: Option<&'a str>,
28 #[serde(borrow)]
29 delta: Option<&'a str>,
30 #[serde(borrow)]
31 item_id: Option<&'a str>,
32 item: Option<Value>,
33}
34
35#[async_trait]
36impl LLMProvider for OpenResponsesProvider {
37 fn name(&self) -> &str {
38 "openresponses"
39 }
40
41 fn supports_streaming(&self) -> bool {
42 true
43 }
44
45 fn supports_non_streaming(&self, _model: &str) -> bool {
46 true
48 }
49
50 fn supports_reasoning(&self, _model: &str) -> bool {
51 self.model_behavior
52 .as_ref()
53 .and_then(|b| b.model_supports_reasoning)
54 .unwrap_or(true) }
56
57 fn supports_reasoning_effort(&self, _model: &str) -> bool {
58 self.model_behavior
59 .as_ref()
60 .and_then(|b| b.model_supports_reasoning_effort)
61 .unwrap_or(true)
62 }
63
64 fn supports_responses_compaction(&self, _model: &str) -> bool {
65 self.supports_compaction_endpoint()
66 }
67
68 fn supports_manual_openai_compaction(&self, _model: &str) -> bool {
75 self.supports_compaction_endpoint()
76 }
77
78 async fn compact_history(&self, model: &str, history: &[Message]) -> Result<Vec<Message>, LLMError> {
79 if !self.supports_compaction_endpoint() {
80 return Err(LLMError::Provider {
81 message: "OpenResponses compact endpoint is not supported for this configured base URL".to_string(),
82 metadata: None,
83 });
84 }
85
86 self.compact_history_request(model, history).await
87 }
88
89 async fn compact_history_with_options(
93 &self,
94 model: &str,
95 history: &[Message],
96 options: &ResponsesCompactionOptions,
97 ) -> Result<Vec<Message>, LLMError> {
98 if !self.supports_compaction_endpoint() {
99 return Err(LLMError::Provider {
100 message: "OpenResponses compact endpoint is not supported for this configured base URL".to_string(),
101 metadata: None,
102 });
103 }
104
105 self.compact_history_request_with_options(model, history, options).await
106 }
107
108 fn supported_models(&self) -> Vec<String> {
109 use vtcode_config::constants::models::openresponses::SUPPORTED_MODELS;
110 SUPPORTED_MODELS.iter().map(|s| s.to_string()).collect()
111 }
112
113 fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
114 if request.model.is_empty() {
115 return Err(LLMError::Provider {
116 message: "Model is required for OpenResponses provider".to_string(),
117 metadata: None,
118 });
119 }
120
121 let supported = self.supported_models();
122 if !supported.contains(&request.model) {
123 return Err(LLMError::Provider {
124 message: format!(
125 "Model '{}' is not supported by OpenResponses provider. Supported models: {}",
126 request.model,
127 supported.join(", ")
128 ),
129 metadata: None,
130 });
131 }
132
133 Ok(())
134 }
135
136 async fn generate(&self, mut request: LLMRequest) -> Result<LLMResponse, LLMError> {
137 if request.model.is_empty() {
138 request.model = self.model.clone();
139 }
140 let model = request.model.clone();
141
142 let payload = self.build_native_payload(&request, false)?;
144 let response = self.send_native_request(&payload).await?;
145
146 if response.status() == reqwest::StatusCode::NOT_FOUND {
148 return self.generate_fallback(request).await;
149 }
150
151 let json: Value = response.json().await.map_err(|e| format_parse_error("OpenResponses", &e))?;
152
153 Self::parse_native_response_payload(json, model)
154 }
155
156 async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream, LLMError> {
157 if request.model.is_empty() {
158 request.model = self.model.clone();
159 }
160 let model = request.model.clone();
161
162 let payload = self.build_native_payload(&request, true)?;
163 let response = self.send_native_request(&payload).await?;
164
165 if response.status() == reqwest::StatusCode::NOT_FOUND {
166 return self.stream_fallback(request).await;
167 }
168
169 let stream = try_stream! {
170 let mut body_stream = response.bytes_stream();
171 let mut buf: Vec<u8> = Vec::new();
172 let mut offset = 0usize;
173 let mut decoder = Utf8StreamDecoder::new();
174 let mut aggregator = crate::providers::shared::StreamAggregator::new(model);
175
176 while let Some(chunk_result) = body_stream.next().await {
177 let chunk = chunk_result.map_err(|e| format_network_error("OpenResponses", &e))?;
178 decoder.push_bytes(&chunk, &mut buf);
179
180 while let Some(event) =
181 crate::providers::shared::next_sse_event(&buf, &mut offset).expect("valid utf-8 stream data")
182 {
183
184 if let Some(data_payload) = crate::providers::shared::extract_data_payload(event) {
185 let trimmed = data_payload.trim();
186 if trimmed.is_empty() || trimmed == "[DONE]" {
187 continue;
188 }
189
190 if let Ok(event) = serde_json::from_str::<NativeStreamEventWire<'_>>(trimmed) {
191 let event_type = event.event_type.unwrap_or("");
192
193 match event_type {
194 "response.output_text.delta" => {
195 if let Some(delta) = event.delta {
196 for ev in aggregator.handle_content(delta) {
198 yield ev;
199 }
200 }
201 }
202 "response.function_call_arguments.delta" => {
203 if let Some(delta) = event.delta {
204 let tc_json = json!([{
205 "index": 0,
206 "id": event.item_id,
207 "function": { "arguments": delta }
208 }]);
209 if let Some(tool_calls) = tc_json.as_array() {
210 aggregator.handle_tool_calls(tool_calls);
211 }
212 }
213 }
214 "response.reasoning.delta" => {
215 if let Some(delta) = event.delta {
217 yield LLMStreamEvent::Reasoning { delta: delta.to_string() };
218 }
219 }
220 "response.reasoning_content.delta" => {
221 if let Some(delta) = event.delta {
223 yield LLMStreamEvent::Reasoning { delta: delta.to_string() };
224 }
225 }
226 "response.reasoning_summary_text.delta" => {
227 if let Some(delta) = event.delta {
229 yield LLMStreamEvent::Reasoning { delta: delta.to_string() };
230 }
231 }
232 "response.output_item.done" => {
236 if let Some(item) = event.item.as_ref()
237 && item.get("type").and_then(Value::as_str) == Some("compaction")
238 {
239 aggregator.append_reasoning_detail(item);
240 }
241 }
242 _ => {}
243 }
244 }
245 }
246 }
247
248 crate::providers::shared::drain_consumed_sse(&mut buf, &mut offset);
251 }
252
253 yield LLMStreamEvent::Completed { response: Box::new(aggregator.finalize()) };
254 };
255
256 Ok(Box::pin(stream))
257 }
258
259 async fn stream_normalized(&self, mut request: LLMRequest) -> Result<LLMNormalizedStream, LLMError> {
260 if request.model.is_empty() {
261 request.model = self.model.clone();
262 }
263 let model = request.model.clone();
264
265 let payload = self.build_native_payload(&request, true)?;
266 let response = self.send_native_request(&payload).await?;
267
268 if response.status() == reqwest::StatusCode::NOT_FOUND {
269 let mut legacy_stream = self.stream_fallback(request).await?;
270 let stream = try_stream! {
271 while let Some(event) = legacy_stream.next().await {
272 for normalized in event?.into_normalized() {
273 yield normalized;
274 }
275 }
276 };
277 return Ok(Box::pin(stream));
278 }
279
280 let emit_reasoning = self.supports_reasoning(&model);
281 Ok(create_responses_normalized_stream(
282 response,
283 ResponsesNormalizedStreamOptions {
284 provider_name: "OpenResponses",
285 model: model.clone(),
286 emit_reasoning,
287 include_cached_prompt_metrics: false,
288 },
289 move |value| Self::parse_native_response_payload(value, model.clone()),
290 ))
291 }
292}