vtcode_llm/providers/
streaming_progress.rs1use std::sync::Arc;
7use std::sync::atomic::{AtomicU8, Ordering};
8use std::time::{Duration, Instant};
9use tracing::warn;
10
11pub type StreamingProgressCallback = Box<dyn Fn(f32) + Send + Sync>;
14
15#[derive(Clone)]
17pub struct StreamingProgressTracker {
18 callback: Option<Arc<StreamingProgressCallback>>,
19 warning_threshold: f32,
20 total_timeout: Duration,
21 start_time: Arc<Instant>,
22 last_reported_progress: Arc<AtomicU8>,
23}
24
25impl StreamingProgressTracker {
26 fn new(total_timeout: Duration) -> Self {
28 Self {
29 callback: None,
30 warning_threshold: 0.8,
31 total_timeout,
32 start_time: Arc::new(Instant::now()),
33 last_reported_progress: Arc::new(AtomicU8::new(0)),
34 }
35 }
36
37 fn with_callback(mut self, callback: StreamingProgressCallback) -> Self {
39 self.callback = Some(Arc::new(callback));
40 self
41 }
42
43 fn with_warning_threshold(mut self, threshold: f32) -> Self {
45 self.warning_threshold = threshold.clamp(0.0, 1.0);
46 self
47 }
48
49 pub fn report_first_chunk(&self) {
51 self.report_progress(0.1);
52 }
53
54 fn report_chunk_received(&self) {
56 let elapsed = self.start_time.elapsed();
57 self.report_progress_with_elapsed(elapsed);
58 }
59
60 fn report_progress_with_elapsed(&self, elapsed: Duration) {
62 if self.total_timeout.as_secs() == 0 {
63 return;
64 }
65
66 let progress = elapsed.as_secs_f32() / self.total_timeout.as_secs_f32();
67 self.report_progress(progress.min(0.99)); }
69
70 fn report_error(&self) {
72 self.report_progress(1.0);
73 }
74
75 fn progress_percent(&self) -> u8 {
77 self.last_reported_progress.load(Ordering::Relaxed)
78 }
79
80 fn elapsed(&self) -> Duration {
82 self.start_time.elapsed()
83 }
84
85 fn is_approaching_timeout(&self) -> bool {
87 let elapsed = self.start_time.elapsed();
88 if self.total_timeout.as_secs() == 0 {
89 return false;
90 }
91
92 let elapsed_progress = elapsed.as_secs_f32() / self.total_timeout.as_secs_f32();
93 let reported_progress = f32::from(self.last_reported_progress.load(Ordering::Relaxed)) / 100.0;
94 elapsed_progress.max(reported_progress) >= self.warning_threshold
95 }
96
97 fn report_progress(&self, progress: f32) {
99 let progress_clamped = progress.clamp(0.0, 1.0);
100 #[allow(
101 clippy::cast_sign_loss,
102 reason = "Intentional compatibility, platform, or test-only suppression."
103 )]
104 let percent = (progress_clamped * 100.0) as u8;
105
106 let last_percent = self.last_reported_progress.load(Ordering::Relaxed);
108 if percent <= last_percent {
109 return;
110 }
111
112 self.last_reported_progress.store(percent, Ordering::Relaxed);
113
114 if let Some(ref callback) = self.callback {
116 callback(progress_clamped);
117 }
118
119 if progress_clamped >= self.warning_threshold && progress_clamped < 1.0 {
121 warn!(
122 "Streaming operation at {:.0}% of timeout limit ({:?}/{:?} elapsed). Approaching timeout.",
123 progress_clamped * 100.0,
124 self.elapsed(),
125 self.total_timeout
126 );
127 }
128 }
129}
130
131#[must_use = "build the progress tracker before discarding the builder"]
133pub struct StreamingProgressBuilder {
134 total_timeout: Duration,
135 callback: Option<StreamingProgressCallback>,
136 warning_threshold: f32,
137}
138
139impl StreamingProgressBuilder {
140 fn new(timeout_secs: u64) -> Self {
142 Self {
143 total_timeout: Duration::from_secs(timeout_secs),
144 callback: None,
145 warning_threshold: 0.8,
146 }
147 }
148
149 pub fn with_duration(duration: Duration) -> Self {
151 Self {
152 total_timeout: duration,
153 callback: None,
154 warning_threshold: 0.8,
155 }
156 }
157
158 pub fn callback(mut self, callback: StreamingProgressCallback) -> Self {
160 self.callback = Some(callback);
161 self
162 }
163
164 fn warning_threshold(mut self, threshold: f32) -> Self {
166 self.warning_threshold = threshold.clamp(0.0, 1.0);
167 self
168 }
169
170 fn build(self) -> StreamingProgressTracker {
172 let mut tracker = StreamingProgressTracker::new(self.total_timeout);
173 if let Some(callback) = self.callback {
174 tracker.callback = Some(Arc::new(callback));
175 }
176 tracker.warning_threshold = self.warning_threshold;
177 tracker
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::sync::Mutex;
185
186 #[test]
187 fn test_progress_tracker_creation() {
188 let tracker = StreamingProgressTracker::new(Duration::from_secs(600));
189 assert_eq!(tracker.progress_percent(), 0);
190 assert!(!tracker.is_approaching_timeout());
191 }
192
193 #[test]
194 fn test_progress_reporting() {
195 let tracker = StreamingProgressTracker::new(Duration::from_secs(100));
196
197 tracker.report_progress_with_elapsed(Duration::from_secs(30));
198 assert_eq!(tracker.progress_percent(), 30);
199
200 tracker.report_progress_with_elapsed(Duration::from_secs(80));
201 assert_eq!(tracker.progress_percent(), 80);
202 }
203
204 #[test]
205 fn test_warning_threshold() {
206 let tracker = StreamingProgressTracker::new(Duration::from_secs(100)).with_warning_threshold(0.8);
207
208 tracker.report_progress_with_elapsed(Duration::from_secs(50));
209 assert!(!tracker.is_approaching_timeout());
210
211 tracker.report_progress_with_elapsed(Duration::from_secs(85));
212 assert!(tracker.is_approaching_timeout());
213 }
214
215 #[test]
216 fn test_callback_execution() {
217 let progress_log = Arc::new(Mutex::new(Vec::new()));
218 let progress_clone = progress_log.clone();
219
220 let tracker =
221 StreamingProgressTracker::new(Duration::from_secs(100)).with_callback(Box::new(move |progress: f32| {
222 progress_clone.lock().unwrap().push(progress);
223 }));
224
225 tracker.report_progress_with_elapsed(Duration::from_secs(30));
226 tracker.report_progress_with_elapsed(Duration::from_secs(60));
227 tracker.report_progress_with_elapsed(Duration::from_secs(90));
228
229 let log = progress_log.lock().unwrap();
230 assert!(!log.is_empty());
231 assert!(log.iter().all(|&p| (0.0..=1.0).contains(&p)));
232 }
233
234 #[test]
235 fn test_builder_pattern() {
236 let tracker = StreamingProgressBuilder::new(300).warning_threshold(0.75).build();
237
238 assert_eq!(tracker.total_timeout.as_secs(), 300);
239 assert!((tracker.warning_threshold - 0.75).abs() < f32::EPSILON);
240 }
241
242 #[test]
243 fn test_zero_timeout_safety() {
244 let tracker = StreamingProgressTracker::new(Duration::from_secs(0));
245 tracker.report_chunk_received(); assert!(!tracker.is_approaching_timeout());
247 }
248
249 #[test]
250 fn test_progress_clamping() {
251 let tracker = StreamingProgressTracker::new(Duration::from_secs(100));
252
253 tracker.report_progress_with_elapsed(Duration::from_secs(150)); assert_eq!(tracker.progress_percent(), 99); tracker.report_error();
257 assert_eq!(tracker.progress_percent(), 100);
258 }
259}