1use serde::Deserialize;
4
5use crate::error::{LlmError, Result};
6use crate::types::{AssistantBlock, FinishReason, FunctionCall, ToolCall, Usage};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct StreamDelta {
11 pub content: String,
12 pub has_tool_calls: bool,
13}
14
15#[derive(Debug, Default)]
16pub(crate) struct StreamAssembler {
17 id: String,
18 content: String,
19 tool_slots: Vec<Option<ToolCallBuilder>>,
20 usage: Option<Usage>,
21 finish_reason: Option<FinishReason>,
22 output_blocks: Vec<AssistantBlock>,
23 saw_choice: bool,
24}
25
26#[derive(Debug, Default, Clone)]
27struct ToolCallBuilder {
28 id: String,
29 kind: String,
30 name: String,
31 arguments: String,
32}
33
34impl StreamAssembler {
35 pub fn apply_json(&mut self, data: &str) -> Result<Option<StreamDelta>> {
36 let chunk: StreamChunk = serde_json::from_str(data)?;
37 if !chunk.id.is_empty() {
38 self.id = chunk.id;
39 }
40 if let Some(usage) = chunk.usage {
41 self.usage = Some(usage);
42 }
43
44 let mut content_changed = false;
45 for choice in chunk.choices {
46 if choice.index != 0 {
47 continue;
48 }
49 self.saw_choice = true;
50 if let Some(reason) = choice.finish_reason {
51 self.finish_reason = Some(reason);
52 }
53 let Some(delta) = choice.delta else {
54 continue;
55 };
56 self.output_blocks.extend(delta.output_blocks);
57 if let Some(piece) = delta.content.filter(|piece| !piece.is_empty()) {
58 self.content.push_str(&piece);
59 content_changed = true;
60 }
61 for tool_delta in delta.tool_calls.unwrap_or_default() {
62 self.merge_tool_delta(tool_delta);
63 }
64 }
65
66 Ok(content_changed.then(|| StreamDelta {
67 content: self.content.clone(),
68 has_tool_calls: self.has_tool_calls(),
69 }))
70 }
71
72 fn has_tool_calls(&self) -> bool {
73 self.tool_slots
74 .iter()
75 .flatten()
76 .any(|tool| !tool.id.is_empty() || !tool.name.is_empty() || !tool.arguments.is_empty())
77 }
78
79 fn merge_tool_delta(&mut self, delta: ToolCallDelta) {
80 let index = delta.index as usize;
81 if self.tool_slots.len() <= index {
82 self.tool_slots.resize_with(index + 1, || None);
83 }
84 let tool = self.tool_slots[index].get_or_insert_with(ToolCallBuilder::default);
85 if let Some(id) = delta.id.filter(|value| !value.is_empty()) {
86 tool.id = id;
87 }
88 if let Some(kind) = delta.kind.filter(|value| !value.is_empty()) {
89 tool.kind = kind;
90 }
91 if let Some(function) = delta.function {
92 if let Some(name) = function.name.filter(|value| !value.is_empty()) {
93 merge_tool_name(&mut tool.name, &name);
94 }
95 if let Some(arguments) = function.arguments {
96 tool.arguments.push_str(&arguments);
97 }
98 }
99 }
100
101 pub fn finish(self) -> Result<crate::types::CompletionResponse> {
102 use crate::types::{ChatMessage, Choice, CompletionResponse, Role};
103
104 if !self.saw_choice {
105 return Err(LlmError::StreamProtocol(
106 "stream completed without choice 0".into(),
107 ));
108 }
109 if self.finish_reason.is_none() {
110 return Err(LlmError::StreamProtocol(
111 "stream completed without finish_reason".into(),
112 ));
113 }
114
115 let tool_calls = self
116 .tool_slots
117 .into_iter()
118 .flatten()
119 .filter(|tool| !tool.name.is_empty() || !tool.arguments.is_empty())
120 .map(|tool| ToolCall {
121 id: if tool.id.is_empty() {
122 format!("call_{:08x}", stable_hash(&tool.name, &tool.arguments))
123 } else {
124 tool.id
125 },
126 kind: if tool.kind.is_empty() {
127 "function".into()
128 } else {
129 tool.kind
130 },
131 function: FunctionCall {
132 name: tool.name,
133 arguments: tool.arguments,
134 },
135 })
136 .collect::<Vec<_>>();
137
138 Ok(CompletionResponse {
139 id: self.id,
140 choices: vec![Choice {
141 index: 0,
142 message: ChatMessage {
143 role: Role::Assistant,
144 content: (!self.content.is_empty()).then_some(self.content),
145 tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
146 tool_call_id: None,
147 name: None,
148 },
149 finish_reason: self.finish_reason,
150 output_blocks: self.output_blocks,
151 }],
152 usage: self.usage,
153 })
154 }
155}
156
157fn stable_hash(left: &str, right: &str) -> u32 {
158 let mut hash = 0x811c_9dc5u32;
159 for byte in left.bytes().chain(right.bytes()) {
160 hash ^= u32::from(byte);
161 hash = hash.wrapping_mul(0x0100_0193);
162 }
163 hash
164}
165
166fn merge_tool_name(current: &mut String, incoming: &str) {
167 if current.is_empty() {
168 current.push_str(incoming);
169 } else if incoming.starts_with(current.as_str()) {
170 *current = incoming.to_string();
171 } else if !current.starts_with(incoming) {
172 current.push_str(incoming);
173 }
174}
175
176#[derive(Debug, Deserialize)]
177struct StreamChunk {
178 #[serde(default)]
179 id: String,
180 #[serde(default)]
181 choices: Vec<StreamChoice>,
182 #[serde(default)]
183 usage: Option<Usage>,
184}
185
186#[derive(Debug, Deserialize)]
187struct StreamChoice {
188 #[serde(default)]
189 index: u32,
190 #[serde(default)]
191 delta: Option<DeltaBody>,
192 #[serde(default)]
193 finish_reason: Option<FinishReason>,
194}
195
196#[derive(Debug, Deserialize)]
197struct DeltaBody {
198 #[serde(default)]
199 content: Option<String>,
200 #[serde(default)]
201 tool_calls: Option<Vec<ToolCallDelta>>,
202 #[serde(default, alias = "content_blocks")]
203 output_blocks: Vec<AssistantBlock>,
204}
205
206#[derive(Debug, Deserialize)]
207struct ToolCallDelta {
208 #[serde(default)]
209 index: u32,
210 #[serde(default)]
211 id: Option<String>,
212 #[serde(default, rename = "type")]
213 kind: Option<String>,
214 #[serde(default)]
215 function: Option<ToolFunctionDelta>,
216}
217
218#[derive(Debug, Deserialize)]
219struct ToolFunctionDelta {
220 #[serde(default)]
221 name: Option<String>,
222 #[serde(default)]
223 arguments: Option<String>,
224}
225
226#[derive(Debug, Default)]
230pub(crate) struct SseDecoder {
231 buffer: Vec<u8>,
232 data_lines: Vec<String>,
233}
234
235impl SseDecoder {
236 pub fn push(&mut self, chunk: &[u8]) -> Result<Vec<String>> {
237 self.buffer.extend_from_slice(chunk);
238 let mut events = Vec::new();
239 while let Some(newline) = self.buffer.iter().position(|byte| *byte == b'\n') {
240 let mut line = self.buffer.drain(..=newline).collect::<Vec<_>>();
241 line.pop();
242 if line.last() == Some(&b'\r') {
243 line.pop();
244 }
245 let line = String::from_utf8(line)?;
246 if line.is_empty() {
247 if !self.data_lines.is_empty() {
248 events.push(self.data_lines.join("\n"));
249 self.data_lines.clear();
250 }
251 } else if let Some(data) = line.strip_prefix("data:") {
252 self.data_lines
253 .push(data.strip_prefix(' ').unwrap_or(data).to_string());
254 }
255 }
256 Ok(events)
257 }
258
259 pub fn finish(self) -> Result<()> {
260 if self.buffer.is_empty() && self.data_lines.is_empty() {
261 return Ok(());
262 }
263 String::from_utf8(self.buffer)?;
264 Err(LlmError::StreamProtocol(
265 "stream ended with an incomplete SSE frame".into(),
266 ))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn assembles_content_and_tool_calls() {
276 let mut assembler = StreamAssembler::default();
277 assert!(assembler
278 .apply_json(r#"{"id":"chatcmpl-1","choices":[{"delta":{"content":"Hi"}}]}"#,)
279 .unwrap()
280 .is_some());
281 assembler
282 .apply_json(r#"{"choices":[{"delta":{"content":" there"}}]}"#)
283 .unwrap();
284 assembler.apply_json(
285 r#"{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"lookup_weather","arguments":""}}]}}]}"#,
286 ).unwrap();
287 assembler.apply_json(
288 r#"{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"Paris\"}"}}]}}]}"#,
289 ).unwrap();
290 assembler
291 .apply_json(r#"{"choices":[{"delta":{},"finish_reason":"tool_calls"}]}"#)
292 .unwrap();
293
294 let response = assembler.finish().unwrap();
295 assert_eq!(response.first_content(), Some("Hi there"));
296 let calls = response.first_tool_calls().unwrap();
297 assert_eq!(calls[0].function.name, "lookup_weather");
298 assert_eq!(calls[0].function.arguments, r#"{"city":"Paris"}"#);
299 }
300
301 #[test]
302 fn drains_lf_and_crlf_frames() {
303 let mut decoder = SseDecoder::default();
304 assert_eq!(
305 decoder
306 .push(b"data: {\"a\":1}\r\n\r\ndata: [DONE]\n\npartial")
307 .unwrap(),
308 [r#"{"a":1}"#, "[DONE]"]
309 );
310 assert!(matches!(decoder.finish(), Err(LlmError::StreamProtocol(_))));
311 }
312
313 #[test]
314 fn joins_multiline_data_and_preserves_usage() {
315 let mut decoder = SseDecoder::default();
316 let frames = decoder
317 .push(b"data: {\"id\":\"x\",\"choices\":[\r\ndata: {\"index\":0,\"delta\":{\"content\":\"hi\"},\"finish_reason\":\"stop\"}],\r\ndata: \"usage\":{\"prompt_tokens\":2,\"completion_tokens\":1,\"total_tokens\":3}}\r\n\r\n")
318 .unwrap();
319 let mut assembler = StreamAssembler::default();
320 assembler.apply_json(&frames[0]).unwrap();
321 let response = assembler.finish().unwrap();
322 assert_eq!(response.first_content(), Some("hi"));
323 assert_eq!(response.usage.unwrap().total_tokens, 3);
324 }
325
326 #[test]
327 fn streams_typed_output_blocks_and_rejects_unknown_kinds() {
328 let mut assembler = StreamAssembler::default();
329 assembler.apply_json(r#"{"choices":[{"delta":{"output_blocks":[{"type":"citation","resource_id":"doc-1","label":"Doc","uri":"docs://doc-1"}]},"finish_reason":"stop"}]}"#).unwrap();
330 let response = assembler.finish().unwrap();
331 assert!(matches!(
332 &response.choices[0].output_blocks[0],
333 AssistantBlock::Citation { resource_id, .. } if resource_id == "doc-1"
334 ));
335 assert!(StreamAssembler::default()
336 .apply_json(
337 r#"{"choices":[{"delta":{"output_blocks":[{"type":"html","html":"bad"}]}}]}"#
338 )
339 .is_err());
340 }
341
342 #[test]
343 fn malformed_json_and_utf8_fail_closed() {
344 assert!(StreamAssembler::default().apply_json("{").is_err());
345 let mut decoder = SseDecoder::default();
346 assert!(matches!(
347 decoder.push(b"data: \xff\n\n"),
348 Err(LlmError::InvalidUtf8(_))
349 ));
350 }
351
352 #[test]
353 fn empty_and_unfinished_assemblies_fail_closed() {
354 assert!(matches!(
355 StreamAssembler::default().finish(),
356 Err(LlmError::StreamProtocol(_))
357 ));
358 let mut assembler = StreamAssembler::default();
359 assembler
360 .apply_json(r#"{"choices":[{"delta":{"content":"partial"}}]}"#)
361 .unwrap();
362 assert!(matches!(
363 assembler.finish(),
364 Err(LlmError::StreamProtocol(_))
365 ));
366 }
367
368 #[test]
369 fn repeated_tool_names_do_not_duplicate() {
370 let mut name = String::new();
371 merge_tool_name(&mut name, "fetch_report");
372 merge_tool_name(&mut name, "fetch_report");
373 assert_eq!(name, "fetch_report");
374 }
375
376 #[test]
377 fn incremental_tool_name_fragments_append() {
378 let mut name = String::new();
379 merge_tool_name(&mut name, "fetch_");
380 merge_tool_name(&mut name, "report");
381 assert_eq!(name, "fetch_report");
382 }
383}