Skip to main content

llm_pipeline/
streaming.rs

1//! Buffered streaming decoder for newline-delimited JSON streams.
2//!
3//! Handles the case where JSON objects are split across TCP chunk boundaries,
4//! which is a common issue with Ollama's streaming API.
5
6use serde_json::Value;
7
8use crate::output_parser::streaming::auto_complete_json;
9
10/// Buffered decoder for newline-delimited JSON streams (NDJSON).
11///
12/// Accumulates raw bytes, splits on newline boundaries, and yields
13/// complete JSON lines. Handles the common case where a single JSON
14/// object is split across multiple network chunks.
15///
16/// # Example
17///
18/// ```
19/// use llm_pipeline::StreamingDecoder;
20///
21/// let mut decoder = StreamingDecoder::new();
22///
23/// // First chunk: partial JSON
24/// let values = decoder.decode(b"{\"response\":");
25/// assert!(values.is_empty());
26///
27/// // Second chunk: completes the line
28/// let values = decoder.decode(b"\"hello\"}\n");
29/// assert_eq!(values.len(), 1);
30/// assert_eq!(values[0]["response"], "hello");
31/// ```
32pub struct StreamingDecoder {
33    buffer: String,
34}
35
36impl StreamingDecoder {
37    /// Create a new empty decoder.
38    pub fn new() -> Self {
39        Self {
40            buffer: String::new(),
41        }
42    }
43
44    /// Feed a raw chunk into the decoder and return any complete JSON lines.
45    ///
46    /// Each returned value is a parsed JSON `Value` from one complete line.
47    /// Incomplete lines are buffered until the next chunk arrives.
48    pub fn decode(&mut self, chunk: &[u8]) -> Vec<Value> {
49        let text = String::from_utf8_lossy(chunk);
50        self.buffer.push_str(&text);
51
52        let mut values = Vec::new();
53
54        while let Some(pos) = self.buffer.find('\n') {
55            let line: String = self.buffer.drain(..=pos).collect();
56            let line = line.trim();
57            if line.is_empty() {
58                continue;
59            }
60            if let Ok(val) = serde_json::from_str::<Value>(line) {
61                values.push(val);
62            }
63        }
64
65        values
66    }
67
68    /// Flush remaining buffer content, attempting to parse it as JSON.
69    ///
70    /// Call this after the stream ends to handle any trailing data
71    /// not terminated by a newline. If direct parsing fails, attempts
72    /// auto-completion of truncated JSON (closing unclosed strings,
73    /// brackets, and braces).
74    pub fn flush(&mut self) -> Option<Value> {
75        let remaining = self.buffer.trim().to_string();
76        self.buffer.clear();
77        if remaining.is_empty() {
78            return None;
79        }
80        // Try direct parse first
81        if let Ok(val) = serde_json::from_str::<Value>(&remaining) {
82            return Some(val);
83        }
84        // Try auto-completing truncated JSON
85        if let Some(completed) = auto_complete_json(&remaining) {
86            return serde_json::from_str::<Value>(&completed).ok();
87        }
88        None
89    }
90}
91
92impl Default for StreamingDecoder {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use serde_json::json;
102
103    #[test]
104    fn test_complete_lines() {
105        let mut decoder = StreamingDecoder::new();
106        let chunk = b"{\"response\":\"hello\"}\n{\"response\":\"world\"}\n";
107        let values = decoder.decode(chunk);
108        assert_eq!(values.len(), 2);
109        assert_eq!(values[0]["response"], "hello");
110        assert_eq!(values[1]["response"], "world");
111    }
112
113    #[test]
114    fn test_split_across_chunks() {
115        let mut decoder = StreamingDecoder::new();
116
117        // First chunk: partial JSON line
118        let values1 = decoder.decode(b"{\"response\":");
119        assert!(values1.is_empty());
120
121        // Second chunk: completes the line
122        let values2 = decoder.decode(b"\"hello\"}\n");
123        assert_eq!(values2.len(), 1);
124        assert_eq!(values2[0]["response"], "hello");
125    }
126
127    #[test]
128    fn test_split_mid_value() {
129        let mut decoder = StreamingDecoder::new();
130
131        let v1 = decoder.decode(b"{\"response\":\"hel");
132        assert!(v1.is_empty());
133
134        let v2 = decoder.decode(b"lo wor");
135        assert!(v2.is_empty());
136
137        let v3 = decoder.decode(b"ld\"}\n");
138        assert_eq!(v3.len(), 1);
139        assert_eq!(v3[0]["response"], "hello world");
140    }
141
142    #[test]
143    fn test_multiple_chunks_multiple_lines() {
144        let mut decoder = StreamingDecoder::new();
145
146        // Chunk contains end of one line and start of another
147        let v1 = decoder.decode(b"{\"a\":1}\n{\"b\":");
148        assert_eq!(v1.len(), 1);
149        assert_eq!(v1[0]["a"], 1);
150
151        let v2 = decoder.decode(b"2}\n");
152        assert_eq!(v2.len(), 1);
153        assert_eq!(v2[0]["b"], 2);
154    }
155
156    #[test]
157    fn test_empty_chunks() {
158        let mut decoder = StreamingDecoder::new();
159        let v = decoder.decode(b"");
160        assert!(v.is_empty());
161        let v = decoder.decode(b"\n\n");
162        assert!(v.is_empty());
163    }
164
165    #[test]
166    fn test_flush_remaining() {
167        let mut decoder = StreamingDecoder::new();
168        decoder.decode(b"{\"done\":true}");
169        // No newline, so nothing returned yet
170        let flushed = decoder.flush();
171        assert!(flushed.is_some());
172        assert_eq!(flushed.unwrap()["done"], json!(true));
173    }
174
175    #[test]
176    fn test_flush_empty() {
177        let mut decoder = StreamingDecoder::new();
178        assert!(decoder.flush().is_none());
179    }
180
181    #[test]
182    fn test_ollama_streaming_simulation() {
183        let mut decoder = StreamingDecoder::new();
184
185        let full_stream = concat!(
186            "{\"model\":\"llama3\",\"response\":\"Hello\"}\n",
187            "{\"model\":\"llama3\",\"response\":\" world\"}\n",
188            "{\"model\":\"llama3\",\"response\":\"!\",\"done\":true}\n",
189        );
190        let bytes = full_stream.as_bytes();
191
192        // Split at awkward positions that cross JSON boundaries
193        let mut all_values = Vec::new();
194        let splits = [15, 37, 60, bytes.len()];
195        let mut start = 0;
196        for &end in &splits {
197            let end = end.min(bytes.len());
198            let chunk = &bytes[start..end];
199            all_values.extend(decoder.decode(chunk));
200            start = end;
201        }
202
203        assert_eq!(all_values.len(), 3);
204        assert_eq!(all_values[0]["response"], "Hello");
205        assert_eq!(all_values[1]["response"], " world");
206        assert_eq!(all_values[2]["response"], "!");
207        assert_eq!(all_values[2]["done"], json!(true));
208    }
209
210    #[test]
211    fn test_streaming_decoder_flush_recovers_truncated() {
212        let mut decoder = StreamingDecoder::new();
213        // Feed truncated JSON (no newline, unclosed brace)
214        decoder.decode(b"{\"name\": \"Alice\", \"age\": 30");
215        let flushed = decoder.flush();
216        assert!(flushed.is_some());
217        let val = flushed.unwrap();
218        assert_eq!(val["name"], "Alice");
219        assert_eq!(val["age"], 30);
220    }
221
222    #[test]
223    fn test_non_json_lines_skipped() {
224        let mut decoder = StreamingDecoder::new();
225        let chunk = b"not json\n{\"ok\":true}\ngarbage\n";
226        let values = decoder.decode(chunk);
227        assert_eq!(values.len(), 1);
228        assert_eq!(values[0]["ok"], json!(true));
229    }
230}