1use std::io;
10
11use serde_json::{Map, Value, json};
12
13use super::ProviderError;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Delta {
18 Text(String),
20 Thinking(String),
22 ToolCall(String),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Stop {
29 EndTurn,
31 ToolUse,
33 MaxTokens,
35 Other(String),
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct Reply {
42 pub content: Vec<Value>,
45 pub stop: Stop,
47 pub input_tokens: Option<u64>,
49 pub output_tokens: Option<u64>,
51}
52
53#[derive(Debug)]
55struct Block {
56 value: Value,
57 index: Option<u64>,
59 input: String,
61 growing: Vec<(&'static str, String)>,
65}
66
67impl Block {
68 fn new(value: Value, index: Option<u64>) -> Self {
69 Self { value, index, input: String::new(), growing: Vec::new() }
70 }
71
72 fn grow(&mut self, field: &'static str, piece: &str) {
74 if let Some((_, text)) = self.growing.iter_mut().find(|(name, _)| *name == field) {
75 text.push_str(piece);
76 return;
77 }
78 let mut text = self.value.get(field).and_then(Value::as_str).unwrap_or_default().to_owned();
79 text.push_str(piece);
80 self.growing.push((field, text));
81 }
82}
83
84pub fn read(
93 lines: impl IntoIterator<Item = io::Result<String>>,
94 on_delta: &mut dyn FnMut(Delta),
95 cancelled: &dyn Fn() -> bool,
96) -> Result<Reply, ProviderError> {
97 let mut blocks: Vec<Block> = Vec::new();
98 let mut reply = Reply { content: Vec::new(), stop: Stop::EndTurn, input_tokens: None, output_tokens: None };
99 let mut data = String::new();
100 let mut complete = false;
101 for line in lines {
102 let line = line.map_err(|error| ProviderError::Unreadable { what: error.to_string() })?;
103 if let Some(rest) = line.strip_prefix("data:") {
104 data.push_str(rest.strip_prefix(' ').unwrap_or(rest));
105 continue;
106 }
107 if !line.is_empty() || data.is_empty() {
108 continue;
109 }
110 let event = std::mem::take(&mut data);
111 if event == "[DONE]" {
112 complete = true;
113 break;
114 }
115 let event: Value = serde_json::from_str(&event)
116 .map_err(|error| ProviderError::Unreadable { what: format!("{error}: {event}") })?;
117 if apply(&event, &mut blocks, &mut reply, on_delta)? {
118 complete = true;
119 break;
120 }
121 if cancelled() {
122 complete = true;
123 break;
124 }
125 }
126 if !complete && cancelled() {
128 complete = true;
129 }
130 if !complete {
131 return Err(ProviderError::Unreadable { what: "the reply ended before the message was complete".to_owned() });
132 }
133 reply.content = blocks.into_iter().map(finish).collect();
134 Ok(reply)
135}
136
137fn apply(
139 event: &Value,
140 blocks: &mut Vec<Block>,
141 reply: &mut Reply,
142 on_delta: &mut dyn FnMut(Delta),
143) -> Result<bool, ProviderError> {
144 let kind = event.get("type").and_then(Value::as_str).unwrap_or_default();
145 match kind {
146 "message_start" => {
147 let usage = event.pointer("/message/usage");
148 reply.input_tokens = usage.and_then(|usage| usage.get("input_tokens")).and_then(Value::as_u64);
149 }
150 "content_block_start" => {
151 let block = event.get("content_block").cloned().unwrap_or_else(|| json!({"type": "text", "text": ""}));
152 if block.get("type").and_then(Value::as_str) == Some("tool_use") {
153 on_delta(Delta::ToolCall(block.get("name").and_then(Value::as_str).unwrap_or_default().to_owned()));
154 }
155 blocks.push(Block::new(block, event.get("index").and_then(Value::as_u64)));
156 }
157 "content_block_delta" => {
158 let index = event.get("index").and_then(Value::as_u64);
162 let at = index
163 .and_then(|index| blocks.iter().rposition(|block| block.index == Some(index)))
164 .or_else(|| blocks.len().checked_sub(1));
165 let Some(block) = at.and_then(|at| blocks.get_mut(at)) else {
166 return Err(ProviderError::Unreadable { what: "a delta arrived before any block".to_owned() });
167 };
168 let delta = event.get("delta").cloned().unwrap_or_default();
169 let text = |field: &str| delta.get(field).and_then(Value::as_str).unwrap_or_default().to_owned();
170 match delta.get("type").and_then(Value::as_str).unwrap_or_default() {
171 "text_delta" => {
172 let piece = text("text");
173 block.grow("text", &piece);
174 on_delta(Delta::Text(piece));
175 }
176 "thinking_delta" => {
177 let piece = text("thinking");
178 block.grow("thinking", &piece);
179 on_delta(Delta::Thinking(piece));
180 }
181 "signature_delta" => block.grow("signature", &text("signature")),
182 "input_json_delta" => block.input.push_str(&text("partial_json")),
183 _ => {}
184 }
185 }
186 "message_delta" => {
187 if let Some(stop) = event.pointer("/delta/stop_reason").and_then(Value::as_str) {
188 reply.stop = match stop {
189 "end_turn" | "stop_sequence" => Stop::EndTurn,
190 "tool_use" => Stop::ToolUse,
191 "max_tokens" => Stop::MaxTokens,
192 other => Stop::Other(other.to_owned()),
193 };
194 }
195 if let Some(tokens) = event.pointer("/usage/output_tokens").and_then(Value::as_u64) {
196 reply.output_tokens = Some(tokens);
197 }
198 if let Some(tokens) = event.pointer("/usage/input_tokens").and_then(Value::as_u64)
202 && (tokens > 0 || reply.input_tokens.is_none())
203 {
204 reply.input_tokens = Some(tokens);
205 }
206 }
207 "message_stop" => return Ok(true),
208 "error" => {
209 let said = event.pointer("/error/message").and_then(Value::as_str).unwrap_or("error").to_owned();
210 return Err(ProviderError::Stopped { said });
211 }
212 _ => {}
213 }
214 Ok(false)
215}
216
217fn finish(block: Block) -> Value {
222 let Block { mut value, input, growing, .. } = block;
223 if let Some(object) = value.as_object_mut() {
224 for (field, text) in growing {
225 object.insert(field.to_owned(), Value::String(text));
226 }
227 }
228 if value.get("type").and_then(Value::as_str) == Some("tool_use")
229 && let Some(object) = value.as_object_mut()
230 {
231 let parsed = if input.trim().is_empty() {
232 Value::Object(Map::new())
233 } else {
234 match serde_json::from_str(&input) {
235 Ok(Value::Object(fields)) => Value::Object(fields),
236 _ => json!({ UNPARSED: input }),
237 }
238 };
239 object.insert("input".to_owned(), parsed);
240 }
241 value
242}
243
244pub const UNPARSED: &str = "unparsed_input";
246
247#[cfg(test)]
248mod tests {
249 use std::io::BufRead as _;
250
251 use super::*;
252
253 fn stream(events: &[Value]) -> String {
255 events
256 .iter()
257 .map(|event| format!("event: {}\ndata: {event}\n\n", event["type"].as_str().unwrap_or("")))
258 .collect()
259 }
260
261 fn collect(text: &str) -> (Result<Reply, ProviderError>, Vec<Delta>) {
262 let mut deltas = Vec::new();
263 let reply = read(text.as_bytes().lines(), &mut |delta| deltas.push(delta), &|| false);
264 (reply, deltas)
265 }
266
267 #[test]
268 fn text_and_thinking_arrive_in_pieces_and_end_up_whole() {
269 let text = stream(&[
270 json!({"type": "message_start", "message": {"usage": {"input_tokens": 14}}}),
271 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}}),
272 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "Short "}}),
273 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "answer."}}),
274 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig"}}),
275 json!({"type": "content_block_stop", "index": 0}),
276 json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}),
277 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Hey"}}),
278 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": " there"}}),
279 json!({"type": "content_block_stop", "index": 1}),
280 json!({"type": "ping"}),
281 json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 9}}),
282 json!({"type": "message_stop"}),
283 ]);
284 let (reply, deltas) = collect(&text);
285 let reply = reply.expect("a reply");
286 assert_eq!(
287 deltas,
288 [
289 Delta::Thinking("Short ".into()),
290 Delta::Thinking("answer.".into()),
291 Delta::Text("Hey".into()),
292 Delta::Text(" there".into())
293 ]
294 );
295 assert_eq!(
296 reply.content,
297 [
298 json!({"type": "thinking", "thinking": "Short answer.", "signature": "sig"}),
299 json!({"type": "text", "text": "Hey there"})
300 ]
301 );
302 assert_eq!(reply.stop, Stop::EndTurn);
303 assert_eq!((reply.input_tokens, reply.output_tokens), (Some(14), Some(9)));
304 }
305
306 #[test]
307 fn the_input_count_said_only_at_the_end_is_kept() {
308 let text = stream(&[
309 json!({"type": "message_start", "message": {"usage": {"input_tokens": 0}}}),
310 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
311 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hi"}}),
312 json!({"type": "content_block_stop", "index": 0}),
313 json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 2817, "output_tokens": 41}}),
314 json!({"type": "message_stop"}),
315 ]);
316 let (reply, _) = collect(&text);
317 let reply = reply.expect("a reply");
318 assert_eq!((reply.input_tokens, reply.output_tokens), (Some(2817), Some(41)));
319 }
320
321 #[test]
322 fn a_zero_at_the_end_does_not_undo_the_starts_count() {
323 let text = stream(&[
324 json!({"type": "message_start", "message": {"usage": {"input_tokens": 1534}}}),
325 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
326 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hi"}}),
327 json!({"type": "content_block_stop", "index": 0}),
328 json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"input_tokens": 0, "output_tokens": 7}}),
329 json!({"type": "message_stop"}),
330 ]);
331 let (reply, _) = collect(&text);
332 let reply = reply.expect("a reply");
333 assert_eq!(reply.input_tokens, Some(1534));
334 }
335
336 #[test]
337 fn a_tool_call_is_announced_by_name_and_its_input_read_from_fragments() {
338 let text = stream(&[
339 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "t1", "name": "read_file", "input": {}}}),
340 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"src/"}}),
341 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "main.rs\"}"}}),
342 json!({"type": "content_block_stop", "index": 0}),
343 json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}),
344 json!({"type": "message_stop"}),
345 ]);
346 let (reply, deltas) = collect(&text);
347 let reply = reply.expect("a reply");
348 assert_eq!(deltas, [Delta::ToolCall("read_file".into())]);
349 assert_eq!(
350 reply.content,
351 [json!({"type": "tool_use", "id": "t1", "name": "read_file", "input": {"path": "src/main.rs"}})]
352 );
353 assert_eq!(reply.stop, Stop::ToolUse);
354 }
355
356 #[test]
357 fn an_error_event_and_a_cut_stream_are_errors() {
358 let error = stream(&[json!({"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}})]);
359 assert_eq!(collect(&error).0, Err(ProviderError::Stopped { said: "Overloaded".into() }));
360 let cut = stream(&[
361 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
362 ]);
363 assert!(matches!(collect(&cut).0, Err(ProviderError::Unreadable { .. })), "no message_stop, no reply");
364 }
365
366 #[test]
367 fn a_cancelled_reply_keeps_what_had_arrived() {
368 let text = stream(&[
369 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
370 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Half"}}),
371 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": " more"}}),
372 ]);
373 let mut seen = 0;
374 let reply = read(text.as_bytes().lines(), &mut |_| seen += 1, &|| true).expect("what arrived");
375 assert_eq!(reply.content, [json!({"type": "text", "text": ""})], "stopped after the first event");
376 assert_eq!(seen, 0);
377 }
378
379 fn cut(text: &str) -> (ProviderError, Vec<Delta>) {
382 let mut deltas = Vec::new();
383 let error = read(text.as_bytes().lines(), &mut |delta| deltas.push(delta), &|| false)
384 .expect_err("an unfinished reply is not a reply");
385 let ProviderError::Unreadable { what } = &error else { panic!("a cut stream, got {error:?}") };
386 assert!(what.contains("ended before the message was complete"), "{what}");
387 (error, deltas)
388 }
389
390 #[test]
391 fn a_stream_that_stops_in_the_middle_of_a_json_line_is_an_error_and_not_a_reply() {
392 cut("event: content_block_start\ndata: {\"type\": \"content_bl");
394 cut("event: content_block_start\ndata: {\"type\": \"message_stop\"}");
396 let whole = stream(&[json!({"type": "message_start", "message": {"usage": {"input_tokens": 3}}})]);
398 let (_, deltas) = cut(&format!("{whole}data: {{\"type\": \"mess"));
399 assert!(deltas.is_empty(), "no piece of an unfinished reply is shown");
400 }
401
402 #[test]
403 fn pieces_of_two_blocks_written_at_once_go_each_to_its_own_block() {
404 let text = stream(&[
405 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}),
406 json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}),
407 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "weigh "}}),
408 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Hello"}}),
409 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "it"}}),
410 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": " there"}}),
411 json!({"type": "message_stop"}),
412 ]);
413 let (reply, _) = collect(&text);
414 assert_eq!(
415 reply.expect("a reply").content,
416 [json!({"type": "thinking", "thinking": "weigh it"}), json!({"type": "text", "text": "Hello there"})],
417 "each piece is in the block its index names"
418 );
419 }
420
421 #[test]
422 fn a_piece_that_names_no_block_goes_to_the_block_being_written() {
423 let text = stream(&[
424 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
425 json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": "second"}}),
426 json!({"type": "content_block_delta", "delta": {"type": "text_delta", "text": " on"}}),
427 json!({"type": "content_block_delta", "index": 7, "delta": {"type": "text_delta", "text": " and on"}}),
428 json!({"type": "message_stop"}),
429 ]);
430 let (reply, _) = collect(&text);
431 assert_eq!(
432 reply.expect("a reply").content,
433 [json!({"type": "text", "text": ""}), json!({"type": "text", "text": "second on and on"})]
434 );
435 }
436
437 #[test]
438 fn one_index_with_two_starts_is_two_blocks_and_neither_is_lost() {
439 let text = stream(&[
440 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "one", "name": "list_dir", "input": {}}}),
441 json!({"type": "content_block_stop", "index": 0}),
442 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "two", "name": "read_file", "input": {}}}),
443 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"a\"}"}}),
444 json!({"type": "message_stop"}),
445 ]);
446 let (reply, deltas) = collect(&text);
447 let reply = reply.expect("a reply");
448 assert_eq!(deltas, [Delta::ToolCall("list_dir".into()), Delta::ToolCall("read_file".into())]);
449 assert_eq!(
450 reply.content,
451 [
452 json!({"type": "tool_use", "id": "one", "name": "list_dir", "input": {}}),
453 json!({"type": "tool_use", "id": "two", "name": "read_file", "input": {"path": "a"}})
454 ]
455 );
456 }
457
458 #[test]
459 fn a_thinking_block_with_no_signature_comes_through_as_it_arrived() {
460 let text = stream(&[
461 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}),
462 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "weighing it"}}),
463 json!({"type": "message_stop"}),
464 ]);
465 let (reply, _) = collect(&text);
466 assert_eq!(reply.expect("a reply").content, [json!({"type": "thinking", "thinking": "weighing it"})]);
467 }
468
469 #[test]
470 fn a_stop_reason_that_is_none_of_the_three_is_kept_as_it_was_said() {
471 for (said, wanted) in [
472 ("refusal", Stop::Other("refusal".to_owned())),
473 ("pause_turn", Stop::Other("pause_turn".to_owned())),
474 ("", Stop::Other(String::new())),
475 ] {
476 let text = stream(&[
477 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
478 json!({"type": "message_delta", "delta": {"stop_reason": said}}),
479 json!({"type": "message_stop"}),
480 ]);
481 assert_eq!(collect(&text).0.expect("a reply").stop, wanted, "{said:?}");
482 }
483 }
484
485 #[test]
486 fn a_tool_input_of_a_megabyte_comes_through_as_it_was_sent() {
487 let path = "p".repeat(1024 * 1024);
488 let text = stream(&[
489 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "big", "name": "read_file", "input": {}}}),
490 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \""}}),
491 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": path}}),
492 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "\"}"}}),
493 json!({"type": "message_stop"}),
494 ]);
495 let (reply, _) = collect(&text);
496 let reply = reply.expect("a reply");
497 assert_eq!(
498 reply.content[0]["input"]["path"].as_str().map(str::len),
499 Some(1024 * 1024),
500 "the whole path is in the input"
501 );
502 }
503
504 #[test]
505 fn a_tool_call_with_no_name_is_announced_empty_and_the_input_is_read_as_it_came() {
506 let text = stream(&[
507 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "x"}}),
508 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"a\"}"}}),
509 json!({"type": "message_stop"}),
510 ]);
511 let (reply, deltas) = collect(&text);
512 assert_eq!(deltas, [Delta::ToolCall("".into())], "the name is empty, not missing");
513 assert_eq!(reply.expect("a reply").content[0]["input"]["path"], "a");
514 }
515
516 #[test]
517 fn a_delta_that_arrives_before_any_block_is_an_error_that_says_so() {
518 let text =
519 stream(&[json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "x"}})]);
520 assert_eq!(
521 collect(&text).0,
522 Err(ProviderError::Unreadable { what: "a delta arrived before any block".to_owned() })
523 );
524 }
525}