gglib_core/sse/
decoder.rs1use anyhow::Result;
10use tracing::debug;
11
12use crate::LlmStreamEvent;
13
14use super::parser::{SseParseResult, parse_sse_frame};
15
16#[derive(Default)]
31pub struct SseStreamDecoder {
32 buf: String,
33 done_sent: bool,
36}
37
38impl SseStreamDecoder {
39 pub fn feed_bytes(&mut self, bytes: &[u8]) -> (Vec<Result<LlmStreamEvent>>, bool) {
49 let text = match std::str::from_utf8(bytes) {
50 Ok(t) => t,
51 Err(e) => {
52 return (
53 vec![Err(anyhow::anyhow!("invalid UTF-8 in LLM SSE stream: {e}"))],
54 true,
55 );
56 }
57 };
58 self.buf.push_str(text);
59 let mut events = Vec::new();
60
61 while let Some(newline_pos) = self.buf.find('\n') {
62 let line = self.buf[..newline_pos].trim_end_matches('\r').to_owned();
63 self.buf.drain(..=newline_pos);
64
65 let Some(data) = line.strip_prefix("data: ") else {
67 continue;
68 };
69
70 match parse_sse_frame(data) {
71 Ok(SseParseResult::Done) => {
72 if !self.done_sent {
73 debug!(
74 "LLM stream ended with [DONE] but no prior finish_reason \
75 — emitting fallback Done with an unknown reason"
76 );
77 events.push(Ok(LlmStreamEvent::Done {
81 finish_reason: None,
82 }));
83 }
84 self.done_sent = true;
85 return (events, true);
86 }
87 Ok(SseParseResult::Events(parsed_events)) => {
88 let mut saw_terminal_error = false;
89 for event in parsed_events {
90 if matches!(event, LlmStreamEvent::Done { .. }) {
91 self.done_sent = true;
92 }
93 if matches!(event, LlmStreamEvent::UpstreamError { .. }) {
94 saw_terminal_error = true;
103 self.done_sent = true;
104 }
105 events.push(Ok(event));
106 }
107 if saw_terminal_error {
108 return (events, true);
109 }
110 }
111 Err(e) => {
112 events.push(Err(e));
113 return (events, true);
114 }
115 }
116 }
117
118 (events, false)
119 }
120
121 #[must_use]
126 pub fn finish(self) -> Option<LlmStreamEvent> {
127 if self.done_sent {
128 None
129 } else {
130 debug!(
131 "LLM byte-stream ended without [DONE] sentinel — emitting fallback Done \
132 with an unknown reason"
133 );
134 Some(LlmStreamEvent::Done {
137 finish_reason: None,
138 })
139 }
140 }
141}
142
143#[cfg(test)]
148mod tests {
149 use anyhow::Result;
150
151 use super::SseStreamDecoder;
152 use crate::LlmStreamEvent;
153
154 fn text_delta_frame(text: &str) -> String {
155 let json = serde_json::json!({
156 "choices": [{
157 "delta": { "content": text },
158 "finish_reason": null
159 }]
160 });
161 format!("data: {json}\n")
162 }
163
164 fn done_frame() -> &'static str {
165 "data: [DONE]\n"
166 }
167
168 fn finish_reason_frame() -> String {
169 let json = serde_json::json!({
170 "choices": [{
171 "delta": {},
172 "finish_reason": "stop"
173 }]
174 });
175 format!("data: {json}\n")
176 }
177
178 fn collect_all(decoder: &mut SseStreamDecoder, input: &str) -> (Vec<LlmStreamEvent>, bool) {
181 let (raw, stop) = decoder.feed_bytes(input.as_bytes());
182 let events: Vec<_> = raw.into_iter().map(Result::unwrap).collect();
183 (events, stop)
184 }
185
186 #[test]
189 fn text_delta_is_emitted() {
190 let mut dec = SseStreamDecoder::default();
191 let (events, stop) = collect_all(&mut dec, &text_delta_frame("hello"));
192 assert!(!stop);
193 assert!(
194 events
195 .iter()
196 .any(|e| matches!(e, LlmStreamEvent::TextDelta { content } if content == "hello"))
197 );
198 }
199
200 #[test]
201 fn done_sentinel_signals_stop_and_emits_fallback() {
202 let mut dec = SseStreamDecoder::default();
203 let (events, stop) = collect_all(&mut dec, done_frame());
204 assert!(stop, "decoder should signal stop on [DONE]");
205 assert!(
206 events
207 .iter()
208 .any(|e| matches!(e, LlmStreamEvent::Done { .. })),
209 "fallback Done should be emitted when no prior finish_reason"
210 );
211 assert!(
212 dec.finish().is_none(),
213 "finish() must return None after a [DONE] sentinel — done_sent must be set"
214 );
215 }
216
217 #[test]
223 fn a_synthesised_done_reports_an_unknown_reason_not_stop() {
224 let mut dec = SseStreamDecoder::default();
225 let (events, _) = collect_all(&mut dec, done_frame());
226 let done = events
227 .iter()
228 .find_map(|e| match e {
229 LlmStreamEvent::Done { finish_reason } => Some(finish_reason),
230 _ => None,
231 })
232 .expect("a fallback Done is still emitted");
233 assert_eq!(*done, None, "must not claim the turn stopped cleanly");
234 }
235
236 #[test]
237 fn a_byte_stream_that_just_stops_reports_an_unknown_reason() {
238 let mut dec = SseStreamDecoder::default();
239 let _ = collect_all(&mut dec, &text_delta_frame("partial"));
240 match dec.finish() {
241 Some(LlmStreamEvent::Done { finish_reason }) => {
242 assert_eq!(finish_reason, None);
243 }
244 other => panic!("expected a fallback Done, got {other:?}"),
245 }
246 }
247
248 #[test]
251 fn a_reported_finish_reason_survives_intact() {
252 let mut dec = SseStreamDecoder::default();
253 let (events, _) = collect_all(&mut dec, &finish_reason_frame());
254 let done = events
255 .iter()
256 .find_map(|e| match e {
257 LlmStreamEvent::Done { finish_reason } => Some(finish_reason),
258 _ => None,
259 })
260 .expect("Done from the reported finish_reason");
261 assert!(done.is_some(), "a real reason must not be discarded");
262 }
263
264 #[test]
265 fn finish_reason_then_done_no_duplicate_done() {
266 let mut dec = SseStreamDecoder::default();
267 let input = format!("{}{}", finish_reason_frame(), done_frame());
268 let (events, stop) = collect_all(&mut dec, &input);
269 assert!(stop);
270 let done_count = events
271 .iter()
272 .filter(|e| matches!(e, LlmStreamEvent::Done { .. }))
273 .count();
274 assert_eq!(done_count, 1, "exactly one Done should be emitted");
275 }
276
277 #[test]
278 fn finish_emits_fallback_when_stream_ends_without_done() {
279 let mut dec = SseStreamDecoder::default();
280 let _ = collect_all(&mut dec, &text_delta_frame("partial"));
281 let fallback = dec.finish();
282 assert!(
283 fallback.is_some(),
284 "finish() should return a fallback Done when stream ends without one"
285 );
286 }
287
288 #[test]
289 fn finish_returns_none_when_done_already_sent() {
290 let mut dec = SseStreamDecoder::default();
291 let _ = collect_all(&mut dec, &finish_reason_frame());
292 assert!(
293 dec.finish().is_none(),
294 "finish() must not emit a second Done"
295 );
296 }
297
298 #[test]
299 fn inline_error_frame_signals_stop_and_suppresses_fallback_done() {
300 let mut dec = SseStreamDecoder::default();
301 let frame = format!(
302 "data: {}\n",
303 serde_json::json!({ "error": { "message": "boom" } })
304 );
305 let (events, stop) = collect_all(&mut dec, &frame);
306 assert!(stop, "inline error frame should signal stop");
307 assert_eq!(events.len(), 1);
308 assert!(matches!(&events[0], LlmStreamEvent::UpstreamError { .. }));
309 assert!(
310 dec.finish().is_none(),
311 "finish() must not append a fallback Done after an inline error"
312 );
313 }
314
315 #[test]
316 fn partial_line_buffered_until_newline_arrives() {
317 let mut dec = SseStreamDecoder::default();
318 let full_frame = text_delta_frame("world");
319
320 let mid = full_frame.len() / 2;
321 let (first_events, stop1) = collect_all(&mut dec, &full_frame[..mid]);
322 assert!(!stop1);
323 assert!(first_events.is_empty(), "no complete line yet");
324
325 let (second_events, stop2) = collect_all(&mut dec, &full_frame[mid..]);
326 assert!(!stop2);
327 assert!(
328 second_events
329 .iter()
330 .any(|e| matches!(e, LlmStreamEvent::TextDelta { .. })),
331 "TextDelta should be emitted once the newline arrives"
332 );
333 }
334}