1use std::io::BufRead;
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 input: String,
59}
60
61pub fn read(
70 lines: impl BufRead,
71 on_delta: &mut dyn FnMut(Delta),
72 cancelled: &dyn Fn() -> bool,
73) -> Result<Reply, ProviderError> {
74 let mut blocks: Vec<Block> = Vec::new();
75 let mut reply = Reply { content: Vec::new(), stop: Stop::EndTurn, input_tokens: None, output_tokens: None };
76 let mut data = String::new();
77 let mut complete = false;
78 for line in lines.lines() {
79 let line = line.map_err(|error| ProviderError::Unreadable { what: error.to_string() })?;
80 if let Some(rest) = line.strip_prefix("data:") {
81 data.push_str(rest.strip_prefix(' ').unwrap_or(rest));
82 continue;
83 }
84 if !line.is_empty() || data.is_empty() {
85 continue;
86 }
87 let event = std::mem::take(&mut data);
88 if event == "[DONE]" {
89 complete = true;
90 break;
91 }
92 let event: Value = serde_json::from_str(&event)
93 .map_err(|error| ProviderError::Unreadable { what: format!("{error}: {event}") })?;
94 if apply(&event, &mut blocks, &mut reply, on_delta)? {
95 complete = true;
96 break;
97 }
98 if cancelled() {
99 complete = true;
100 break;
101 }
102 }
103 if !complete {
104 return Err(ProviderError::Unreadable { what: "the reply ended before the message was complete".to_owned() });
105 }
106 reply.content = blocks.into_iter().map(finish).collect();
107 Ok(reply)
108}
109
110fn apply(
112 event: &Value,
113 blocks: &mut Vec<Block>,
114 reply: &mut Reply,
115 on_delta: &mut dyn FnMut(Delta),
116) -> Result<bool, ProviderError> {
117 let kind = event.get("type").and_then(Value::as_str).unwrap_or_default();
118 match kind {
119 "message_start" => {
120 let usage = event.pointer("/message/usage");
121 reply.input_tokens = usage.and_then(|usage| usage.get("input_tokens")).and_then(Value::as_u64);
122 }
123 "content_block_start" => {
124 let block = event.get("content_block").cloned().unwrap_or_else(|| json!({"type": "text", "text": ""}));
125 if block.get("type").and_then(Value::as_str) == Some("tool_use") {
126 on_delta(Delta::ToolCall(block.get("name").and_then(Value::as_str).unwrap_or_default().to_owned()));
127 }
128 blocks.push(Block { value: block, input: String::new() });
129 }
130 "content_block_delta" => {
131 let Some(block) = blocks.last_mut() else {
132 return Err(ProviderError::Unreadable { what: "a delta arrived before any block".to_owned() });
133 };
134 let delta = event.get("delta").cloned().unwrap_or_default();
135 let text = |field: &str| delta.get(field).and_then(Value::as_str).unwrap_or_default().to_owned();
136 match delta.get("type").and_then(Value::as_str).unwrap_or_default() {
137 "text_delta" => {
138 let piece = text("text");
139 append(&mut block.value, "text", &piece);
140 on_delta(Delta::Text(piece));
141 }
142 "thinking_delta" => {
143 let piece = text("thinking");
144 append(&mut block.value, "thinking", &piece);
145 on_delta(Delta::Thinking(piece));
146 }
147 "signature_delta" => append(&mut block.value, "signature", &text("signature")),
148 "input_json_delta" => block.input.push_str(&text("partial_json")),
149 _ => {}
150 }
151 }
152 "message_delta" => {
153 if let Some(stop) = event.pointer("/delta/stop_reason").and_then(Value::as_str) {
154 reply.stop = match stop {
155 "end_turn" | "stop_sequence" => Stop::EndTurn,
156 "tool_use" => Stop::ToolUse,
157 "max_tokens" => Stop::MaxTokens,
158 other => Stop::Other(other.to_owned()),
159 };
160 }
161 if let Some(tokens) = event.pointer("/usage/output_tokens").and_then(Value::as_u64) {
162 reply.output_tokens = Some(tokens);
163 }
164 }
165 "message_stop" => return Ok(true),
166 "error" => {
167 let said = event.pointer("/error/message").and_then(Value::as_str).unwrap_or("error").to_owned();
168 return Err(ProviderError::Stopped { said });
169 }
170 _ => {}
171 }
172 Ok(false)
173}
174
175fn append(block: &mut Value, field: &str, piece: &str) {
177 if let Some(object) = block.as_object_mut() {
178 let mut text = object.get(field).and_then(Value::as_str).unwrap_or_default().to_owned();
179 text.push_str(piece);
180 object.insert(field.to_owned(), Value::String(text));
181 }
182}
183
184fn finish(block: Block) -> Value {
188 let Block { mut value, input } = block;
189 if value.get("type").and_then(Value::as_str) == Some("tool_use")
190 && let Some(object) = value.as_object_mut()
191 {
192 let parsed = if input.trim().is_empty() {
193 Value::Object(Map::new())
194 } else {
195 serde_json::from_str(&input).unwrap_or(Value::String(input))
196 };
197 object.insert("input".to_owned(), parsed);
198 }
199 value
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn stream(events: &[Value]) -> String {
208 events
209 .iter()
210 .map(|event| format!("event: {}\ndata: {event}\n\n", event["type"].as_str().unwrap_or("")))
211 .collect()
212 }
213
214 fn collect(text: &str) -> (Result<Reply, ProviderError>, Vec<Delta>) {
215 let mut deltas = Vec::new();
216 let reply = read(text.as_bytes(), &mut |delta| deltas.push(delta), &|| false);
217 (reply, deltas)
218 }
219
220 #[test]
221 fn text_and_thinking_arrive_in_pieces_and_end_up_whole() {
222 let text = stream(&[
223 json!({"type": "message_start", "message": {"usage": {"input_tokens": 14}}}),
224 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}}),
225 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "Short "}}),
226 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "answer."}}),
227 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig"}}),
228 json!({"type": "content_block_stop", "index": 0}),
229 json!({"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}),
230 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "Hey"}}),
231 json!({"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": " there"}}),
232 json!({"type": "content_block_stop", "index": 1}),
233 json!({"type": "ping"}),
234 json!({"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 9}}),
235 json!({"type": "message_stop"}),
236 ]);
237 let (reply, deltas) = collect(&text);
238 let reply = reply.expect("a reply");
239 assert_eq!(
240 deltas,
241 [
242 Delta::Thinking("Short ".into()),
243 Delta::Thinking("answer.".into()),
244 Delta::Text("Hey".into()),
245 Delta::Text(" there".into())
246 ]
247 );
248 assert_eq!(
249 reply.content,
250 [
251 json!({"type": "thinking", "thinking": "Short answer.", "signature": "sig"}),
252 json!({"type": "text", "text": "Hey there"})
253 ]
254 );
255 assert_eq!(reply.stop, Stop::EndTurn);
256 assert_eq!((reply.input_tokens, reply.output_tokens), (Some(14), Some(9)));
257 }
258
259 #[test]
260 fn a_tool_call_is_announced_by_name_and_its_input_read_from_fragments() {
261 let text = stream(&[
262 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "tool_use", "id": "t1", "name": "read_file", "input": {}}}),
263 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "{\"path\": \"src/"}}),
264 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "input_json_delta", "partial_json": "main.rs\"}"}}),
265 json!({"type": "content_block_stop", "index": 0}),
266 json!({"type": "message_delta", "delta": {"stop_reason": "tool_use"}}),
267 json!({"type": "message_stop"}),
268 ]);
269 let (reply, deltas) = collect(&text);
270 let reply = reply.expect("a reply");
271 assert_eq!(deltas, [Delta::ToolCall("read_file".into())]);
272 assert_eq!(
273 reply.content,
274 [json!({"type": "tool_use", "id": "t1", "name": "read_file", "input": {"path": "src/main.rs"}})]
275 );
276 assert_eq!(reply.stop, Stop::ToolUse);
277 }
278
279 #[test]
280 fn an_error_event_and_a_cut_stream_are_errors() {
281 let error = stream(&[json!({"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}})]);
282 assert_eq!(collect(&error).0, Err(ProviderError::Stopped { said: "Overloaded".into() }));
283 let cut = stream(&[
284 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
285 ]);
286 assert!(matches!(collect(&cut).0, Err(ProviderError::Unreadable { .. })), "no message_stop, no reply");
287 }
288
289 #[test]
290 fn a_cancelled_reply_keeps_what_had_arrived() {
291 let text = stream(&[
292 json!({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}),
293 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Half"}}),
294 json!({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": " more"}}),
295 ]);
296 let mut seen = 0;
297 let reply = read(text.as_bytes(), &mut |_| seen += 1, &|| true).expect("what arrived");
298 assert_eq!(reply.content, [json!({"type": "text", "text": ""})], "stopped after the first event");
299 assert_eq!(seen, 0);
300 }
301}