1use crate::error::{LiteLLMError, Result};
2use crate::http::MAX_SSE_BUFFER_SIZE;
3use crate::providers::gemini;
4use crate::types::{Reasoning, Usage};
5use bytes::Bytes;
6use futures_util::stream::{Stream, StreamExt, TryStreamExt};
7use serde_json::Value;
8use std::pin::Pin;
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio_util::io::StreamReader;
11
12#[derive(Debug, Clone)]
13pub struct ChatStreamChunk {
14 pub content: String,
16 pub reasoning: Option<Reasoning>,
18 pub raw: Option<Value>,
19 pub usage: Option<Usage>,
20}
21
22pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatStreamChunk>> + Send>>;
23
24#[derive(Debug, Clone)]
25struct SseEvent {
26 event: Option<String>,
27 data: String,
28}
29
30type SseEventStream = Pin<Box<dyn Stream<Item = Result<SseEvent>> + Send>>;
31
32fn sse_event_stream<S>(stream: S) -> SseEventStream
33where
34 S: Stream<Item = std::result::Result<Bytes, reqwest::Error>> + Send + Unpin + 'static,
35{
36 let s = async_stream::try_stream! {
37 let stream = stream.map_err(std::io::Error::other);
38 let reader = StreamReader::new(stream);
39 let mut lines = BufReader::new(reader).lines();
40
41 let mut event_name: Option<String> = None;
42 let mut data_buf = String::new();
43
44 while let Some(line) = lines.next_line().await.map_err(LiteLLMError::from)? {
45 if line.is_empty() {
46 if !data_buf.is_empty() {
47 let data = std::mem::take(&mut data_buf);
48 let event = event_name.take();
49 yield SseEvent { event, data };
50 } else {
51 event_name = None;
52 }
53 continue;
54 }
55
56 if line.starts_with(':') {
57 continue;
58 }
59
60 let (field, value) = if let Some((field, value)) = line.split_once(':') {
61 (field, value.strip_prefix(' ').unwrap_or(value))
62 } else {
63 (line.as_str(), "")
64 };
65
66 match field {
67 "event" => {
68 event_name = Some(value.to_string());
69 }
70 "data" => {
71 if !data_buf.is_empty() {
72 data_buf.push('\n');
73 }
74 data_buf.push_str(value);
75 if data_buf.len() > MAX_SSE_BUFFER_SIZE {
76 Err(LiteLLMError::http(format!(
77 "SSE data buffer exceeded maximum size of {} bytes",
78 MAX_SSE_BUFFER_SIZE
79 )))?;
80 }
81 }
82 _ => {}
83 }
84 }
85
86 if !data_buf.is_empty() {
87 let data = std::mem::take(&mut data_buf);
88 let event = event_name.take();
89 yield SseEvent { event, data };
90 }
91 };
92 Box::pin(s)
93}
94
95pub fn parse_sse_stream<S>(stream: S) -> ChatStream
100where
101 S: Stream<Item = std::result::Result<Bytes, reqwest::Error>> + Send + Unpin + 'static,
102{
103 let s = async_stream::try_stream! {
104 let mut events = sse_event_stream(stream);
105 while let Some(event) = events.next().await {
106 let event = event?;
107 let data = event.data.trim();
108 if data == "[DONE]" {
109 return;
110 }
111 let value: Value = serde_json::from_str(data)
112 .map_err(|e| LiteLLMError::Parse(e.to_string()))?;
113 let usage = parse_usage(&value);
114 let content = value
115 .pointer("/choices/0/delta/content")
116 .and_then(|v| v.as_str())
117 .map(|s| s.to_string());
118 let reasoning_text = value
119 .pointer("/choices/0/delta/reasoning_content")
120 .and_then(|v| v.as_str())
121 .or_else(|| {
122 value
123 .pointer("/choices/0/delta/reasoning")
124 .and_then(|v| v.as_str())
125 })
126 .map(|s| s.to_string());
127 let reasoning_details = value
128 .pointer("/choices/0/delta/reasoning_details")
129 .and_then(|v| v.as_array())
130 .cloned();
131 let reasoning = reasoning_details
132 .and_then(Reasoning::from_details)
133 .or_else(|| reasoning_text.and_then(Reasoning::from_text));
134 let content = content.unwrap_or_default();
135 yield ChatStreamChunk {
136 content,
137 reasoning,
138 raw: Some(value),
139 usage,
140 };
141 }
142 };
143 Box::pin(s)
144}
145
146pub fn parse_responses_sse_stream<S>(stream: S) -> ChatStream
150where
151 S: Stream<Item = std::result::Result<Bytes, reqwest::Error>> + Send + Unpin + 'static,
152{
153 let s = async_stream::try_stream! {
154 let mut events = sse_event_stream(stream);
155 let mut accumulated_content = String::new();
156 let mut refusal = String::new();
157 let mut usage = Usage::default();
158 while let Some(event) = events.next().await {
159 let event = event?;
160 let data = event.data.trim();
161 if data == "[DONE]" {
162 if !refusal.is_empty() {
163 Err(LiteLLMError::Refusal { text: refusal.clone(), usage: Box::new(usage.clone()) })?;
164 }
165 return;
166 }
167 let value: Value = serde_json::from_str(data)
168 .map_err(|e| LiteLLMError::Parse(e.to_string()))?;
169 let kind = value.get("type").and_then(Value::as_str).or(event.event.as_deref());
170 if kind == Some("error") || value.get("type").and_then(Value::as_str) == Some("error") {
171 let message = value
172 .get("message")
173 .or_else(|| value.pointer("/error/message"))
174 .and_then(Value::as_str)
175 .unwrap_or("Responses API stream error");
176 Err(LiteLLMError::http(message))?;
177 }
178 if matches!(kind, Some("response.failed" | "response.incomplete")) {
179 if kind == Some("response.incomplete") {
180 if let Some(incomplete_usage) = value.get("response").and_then(parse_responses_usage) {
181 usage = incomplete_usage;
182 }
183 }
184 let message = value
185 .pointer("/response/error/message")
186 .and_then(Value::as_str)
187 .or_else(|| value.pointer("/response/incomplete_details/reason").and_then(Value::as_str))
188 .unwrap_or("Responses API did not complete the response");
189 if kind == Some("response.incomplete") {
190 Err(LiteLLMError::Truncated { text: accumulated_content.clone(), usage: Box::new(usage.clone()) })?;
191 }
192 Err(LiteLLMError::http(message))?;
193 }
194 if kind == Some("response.refusal.delta") {
195 refusal.push_str(value.get("delta").and_then(Value::as_str).unwrap_or_default());
196 continue;
197 }
198 if kind == Some("response.refusal.done") {
199 if let Some(text) = value.get("refusal").and_then(Value::as_str) {
200 refusal = text.to_owned();
201 }
202 if !refusal.is_empty() {
203 Err(LiteLLMError::Refusal { text: refusal.clone(), usage: Box::new(usage.clone()) })?;
204 }
205 continue;
206 }
207 let content = if kind == Some("response.output_text.delta") {
208 value.get("delta").and_then(Value::as_str).unwrap_or_default().to_string()
209 } else { String::new() };
210 accumulated_content.push_str(&content);
211 let reasoning = if kind == Some("response.reasoning_summary_text.delta") {
212 value.get("delta").and_then(Value::as_str).map(str::to_owned).and_then(Reasoning::from_text)
213 } else { None };
214 let event_usage = if kind == Some("response.completed") {
215 value.get("response").and_then(parse_responses_usage)
216 } else { None };
217 if let Some(completed_usage) = event_usage.clone() {
218 usage = completed_usage;
219 }
220 if kind == Some("response.completed") {
221 let response = value.get("response").unwrap_or(&Value::Null);
222 let completed_refusal = response
223 .get("output").and_then(Value::as_array).into_iter().flatten()
224 .flat_map(|item| item.get("content").and_then(Value::as_array).into_iter().flatten())
225 .filter(|part| part.get("type").and_then(Value::as_str) == Some("refusal"))
226 .filter_map(|part| part.get("refusal").and_then(Value::as_str))
227 .collect::<String>();
228 if !completed_refusal.is_empty() {
229 Err(LiteLLMError::Refusal { text: completed_refusal, usage: Box::new(usage.clone()) })?;
230 }
231 if !refusal.is_empty() {
232 Err(LiteLLMError::Refusal { text: refusal.clone(), usage: Box::new(usage.clone()) })?;
233 }
234 }
235 if !content.is_empty() || reasoning.is_some() || event_usage.is_some()
236 || kind.map(|k| k.starts_with("response.function_call_arguments.") || k.starts_with("response.output_item.")).unwrap_or(false) {
237 yield ChatStreamChunk { content, reasoning, raw: Some(value), usage: event_usage };
238 }
239 }
240 if !refusal.is_empty() {
241 Err(LiteLLMError::Refusal { text: refusal, usage: Box::new(usage) })?;
242 }
243 };
244 Box::pin(s)
245}
246
247fn parse_responses_usage(value: &Value) -> Option<Usage> {
248 let usage = value.get("usage")?.as_object()?;
249 Some(Usage {
250 prompt_tokens: usage.get("input_tokens").and_then(Value::as_u64),
251 completion_tokens: usage.get("output_tokens").and_then(Value::as_u64),
252 thoughts_tokens: usage
253 .get("output_tokens_details")
254 .and_then(|v| v.get("reasoning_tokens"))
255 .and_then(Value::as_u64),
256 total_tokens: usage.get("total_tokens").and_then(Value::as_u64),
257 cost_usd: None,
258 ..Default::default()
259 })
260}
261
262pub fn parse_anthropic_sse_stream<S>(stream: S) -> ChatStream
267where
268 S: Stream<Item = std::result::Result<Bytes, reqwest::Error>> + Send + Unpin + 'static,
269{
270 let s = async_stream::try_stream! {
271 let mut events = sse_event_stream(stream);
272 while let Some(event) = events.next().await {
273 let event = event?;
274 let data = event.data.trim();
275 if data == "[DONE]" {
276 return;
277 }
278 let value: Value = serde_json::from_str(data)
279 .map_err(|e| LiteLLMError::Parse(e.to_string()))?;
280 let usage = parse_usage(&value);
281 if event.event.as_deref() == Some("content_block_delta") {
282 let text = value
283 .pointer("/delta/text")
284 .and_then(|v| v.as_str())
285 .map(|s| s.to_string());
286 let reasoning = value
287 .pointer("/delta/thinking")
288 .and_then(|v| v.as_str())
289 .map(|s| s.to_string())
290 .and_then(Reasoning::from_text);
291 let content = text.unwrap_or_default();
292 if !content.is_empty() || reasoning.is_some() {
293 yield ChatStreamChunk {
294 content,
295 reasoning,
296 raw: Some(value),
297 usage,
298 };
299 }
300 }
301 }
302 };
303 Box::pin(s)
304}
305
306pub fn parse_gemini_sse_stream<S>(stream: S) -> ChatStream
317where
318 S: Stream<Item = std::result::Result<Bytes, reqwest::Error>> + Send + Unpin + 'static,
319{
320 let s = async_stream::try_stream! {
321 let mut events = sse_event_stream(stream);
322 while let Some(event) = events.next().await {
323 let event = event?;
324 let data = event.data.trim();
325 if data.is_empty() {
326 continue;
327 }
328 let value: Value = serde_json::from_str(data)
329 .map_err(|e| LiteLLMError::Parse(e.to_string()))?;
330 let finished = value
331 .get("candidates")
332 .and_then(Value::as_array)
333 .is_some_and(|c| c.iter().any(|c| c.get("finishReason").is_some()));
334 let usage = (finished && value.get("usageMetadata").is_some())
335 .then(|| gemini::parse_usage(&value));
336 let (content, reasoning) = gemini::split_text_and_reasoning(&value);
337 yield ChatStreamChunk {
338 content,
339 reasoning,
340 raw: Some(value),
341 usage,
342 };
343 }
344 };
345 Box::pin(s)
346}
347
348fn parse_usage(value: &Value) -> Option<Usage> {
349 let usage = value.get("usage")?.as_object()?;
350 let prompt_tokens = usage.get("prompt_tokens").and_then(|v| v.as_u64());
351 let completion_tokens = usage.get("completion_tokens").and_then(|v| v.as_u64());
352 let total_tokens = usage.get("total_tokens").and_then(|v| v.as_u64());
353 let cost_usd = usage
354 .get("cost")
355 .and_then(|v| v.as_f64())
356 .or_else(|| usage.get("cost").and_then(|v| v.as_str())?.parse().ok())
357 .or_else(|| usage.get("cost_usd").and_then(|v| v.as_f64()))
358 .or_else(|| usage.get("total_cost").and_then(|v| v.as_f64()));
359 Some(Usage {
360 prompt_tokens,
361 completion_tokens,
362 thoughts_tokens: None,
363 total_tokens,
364 cost_usd,
365 ..Default::default()
366 })
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use bytes::Bytes;
373 use futures_util::stream;
374
375 #[tokio::test]
376 async fn parse_sse_basic() {
377 let data = "data: {\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\n\n\
378 data: {\"choices\":[{\"delta\":{\"content\":\" World\"}}]}\n\n\
379 data: [DONE]\n\n";
380 let bytes_stream = stream::iter(vec![Ok(Bytes::from(data))]);
381 let mut chat_stream = parse_sse_stream(bytes_stream);
382
383 let chunk1 = chat_stream.next().await.unwrap().unwrap();
384 assert_eq!(chunk1.content, "Hello");
385
386 let chunk2 = chat_stream.next().await.unwrap().unwrap();
387 assert_eq!(chunk2.content, " World");
388
389 assert!(chat_stream.next().await.is_none());
390 }
391
392 #[tokio::test]
393 async fn parse_responses_sse_text_function_and_usage() {
394 let data = "event: response.output_text.delta\n\
395 data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello\"}\n\n\
396 event: response.function_call_arguments.delta\n\
397 data: {\"type\":\"response.function_call_arguments.delta\",\"delta\":\"{}\"}\n\n\
398 event: response.completed\n\
399 data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":3,\"output_tokens\":5,\"total_tokens\":8,\"output_tokens_details\":{\"reasoning_tokens\":2}}}}\n\n";
400 let mut stream = parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(data))]));
401 assert_eq!(stream.next().await.unwrap().unwrap().content, "Hello");
402 assert_eq!(
403 stream.next().await.unwrap().unwrap().raw.unwrap()["delta"],
404 "{}"
405 );
406 let usage = stream.next().await.unwrap().unwrap().usage.unwrap();
407 assert_eq!(usage.prompt_tokens, Some(3));
408 assert_eq!(usage.thoughts_tokens, Some(2));
409 }
410
411 #[tokio::test]
412 async fn parse_responses_sse_preserves_function_items_and_fails_status_events() {
413 let data = "data: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"function_call\",\"call_id\":\"call_1\",\"name\":\"weather\"}}\n\n\
414 data: {\"type\":\"response.function_call_arguments.done\",\"call_id\":\"call_1\",\"arguments\":\"{}\"}\n\n";
415 let mut stream = parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(data))]));
416 assert_eq!(
417 stream.next().await.unwrap().unwrap().raw.unwrap()["item"]["call_id"],
418 "call_1"
419 );
420 assert_eq!(
421 stream.next().await.unwrap().unwrap().raw.unwrap()["arguments"],
422 "{}"
423 );
424
425 let failed = "data: {\"type\":\"response.failed\",\"response\":{\"error\":{\"message\":\"blocked\"}}}\n\n";
426 let mut stream = parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(failed))]));
427 assert!(stream
428 .next()
429 .await
430 .unwrap()
431 .unwrap_err()
432 .to_string()
433 .contains("blocked"));
434
435 let event_error =
436 "event: error\ndata: {\"type\":\"error\",\"message\":\"transport failed\"}\n\n";
437 let mut stream =
438 parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(event_error))]));
439 assert!(stream
440 .next()
441 .await
442 .unwrap()
443 .unwrap_err()
444 .to_string()
445 .contains("transport failed"));
446
447 let refusal = "data: {\"type\":\"response.refusal.delta\",\"delta\":\"no\"}\n\n\
448 data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"content\":[{\"type\":\"refusal\",\"refusal\":\"no\"}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n";
449 let mut stream = parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(refusal))]));
450 match stream.next().await.unwrap().unwrap_err() {
451 LiteLLMError::Refusal { text, usage } => {
452 assert_eq!(text, "no");
453 assert_eq!(usage.total_tokens, Some(2));
454 }
455 other => panic!("unexpected error: {other:?}"),
456 }
457
458 let refusal_done = concat!(
459 "data: {\"type\":\"response.refusal.delta\",\"delta\":\"no\"}\n\n",
460 "data: {\"type\":\"response.refusal.done\",\"refusal\":\"no\"}\n\n",
461 );
462 let mut stream =
463 parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(refusal_done))]));
464 match stream.next().await.unwrap().unwrap_err() {
465 LiteLLMError::Refusal { text, .. } => assert_eq!(text, "no"),
466 other => panic!("unexpected error: {other:?}"),
467 }
468
469 let refusal_completed = concat!(
470 "data: {\"type\":\"response.refusal.delta\",\"delta\":\"no\"}\n\n",
471 "data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n",
472 );
473 let mut stream =
474 parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(refusal_completed))]));
475 match stream.next().await.unwrap().unwrap_err() {
476 LiteLLMError::Refusal { text, usage } => {
477 assert_eq!(text, "no");
478 assert_eq!(usage.total_tokens, Some(2));
479 }
480 other => panic!("unexpected error: {other:?}"),
481 }
482
483 let refusal_eof = "data: {\"type\":\"response.refusal.delta\",\"delta\":\"no\"}\n\n";
484 let mut stream =
485 parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(refusal_eof))]));
486 match stream.next().await.unwrap().unwrap_err() {
487 LiteLLMError::Refusal { text, .. } => assert_eq!(text, "no"),
488 other => panic!("unexpected error: {other:?}"),
489 }
490
491 let incomplete = concat!(
492 "data: {\"type\":\"response.output_text.delta\",\"delta\":\"partial\"}\n\n",
493 "data: {\"type\":\"response.incomplete\",\"response\":{\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"max_output_tokens\"},\"usage\":{\"input_tokens\":4,\"output_tokens\":6,\"total_tokens\":10}}}\n\n",
494 );
495 let mut stream =
496 parse_responses_sse_stream(stream::iter(vec![Ok(Bytes::from(incomplete))]));
497 assert_eq!(stream.next().await.unwrap().unwrap().content, "partial");
498 match stream.next().await.unwrap().unwrap_err() {
499 LiteLLMError::Truncated { text, usage } => {
500 assert_eq!(text, "partial");
501 assert_eq!(usage.prompt_tokens, Some(4));
502 assert_eq!(usage.total_tokens, Some(10));
503 }
504 other => panic!("unexpected error: {other:?}"),
505 }
506 }
507
508 #[tokio::test]
509 async fn parse_anthropic_sse_basic() {
510 let data = "event: content_block_delta\n\
511 data: {\"delta\":{\"text\":\"Hello\"}}\n\n\
512 event: content_block_delta\n\
513 data: {\"delta\":{\"text\":\" World\"}}\n\n";
514 let bytes_stream = stream::iter(vec![Ok(Bytes::from(data))]);
515 let mut chat_stream = parse_anthropic_sse_stream(bytes_stream);
516
517 let chunk1 = chat_stream.next().await.unwrap().unwrap();
518 assert_eq!(chunk1.content, "Hello");
519
520 let chunk2 = chat_stream.next().await.unwrap().unwrap();
521 assert_eq!(chunk2.content, " World");
522 }
523
524 #[tokio::test]
525 async fn parse_sse_handles_split_chunks() {
526 let chunk1 = "data: {\"choices\":[{\"delta\":{\"con";
528 let chunk2 = "tent\":\"Split\"}}]}\n\ndata: [DONE]\n\n";
529 let bytes_stream = stream::iter(vec![Ok(Bytes::from(chunk1)), Ok(Bytes::from(chunk2))]);
530 let mut chat_stream = parse_sse_stream(bytes_stream);
531
532 let chunk = chat_stream.next().await.unwrap().unwrap();
533 assert_eq!(chunk.content, "Split");
534
535 assert!(chat_stream.next().await.is_none());
536 }
537}