use crate::error::{Error, Result};
use crate::types::{BatchResultLine, ChatCompletionChunk, ResponsesStreamEvent};
use bytes::{Bytes, BytesMut};
use futures::{Stream, StreamExt};
use serde::de::DeserializeOwned;
const DONE: &str = "[DONE]";
pub(crate) fn sse_chunks<S>(byte_stream: S) -> impl Stream<Item = Result<ChatCompletionChunk>>
where
S: Stream<Item = reqwest::Result<Bytes>> + Unpin,
{
let mut buf = BytesMut::new();
let mut byte_stream = byte_stream;
let mut done = false;
futures::stream::poll_fn(move |cx| {
use std::task::Poll;
loop {
if done {
return Poll::Ready(None);
}
if let Some(line) = take_line(&mut buf) {
match parse_sse_data_line(&line) {
SseLine::Done => {
done = true;
return Poll::Ready(None);
}
SseLine::Skip => continue,
SseLine::Json(json) => {
let parsed =
serde_json::from_str::<ChatCompletionChunk>(&json).map_err(Error::from);
return Poll::Ready(Some(parsed));
}
}
}
match byte_stream.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => {
buf.extend_from_slice(&chunk);
}
Poll::Ready(Some(Err(e))) => {
done = true;
return Poll::Ready(Some(Err(Error::Http(e))));
}
Poll::Ready(None) => {
if let Some(line) = take_remaining(&mut buf) {
match parse_sse_data_line(&line) {
SseLine::Done => {}
SseLine::Skip => {}
SseLine::Json(json) => {
done = true;
let parsed = serde_json::from_str::<ChatCompletionChunk>(&json)
.map_err(Error::from);
return Poll::Ready(Some(parsed));
}
}
}
done = true;
return Poll::Ready(None);
}
}
}
})
}
pub(crate) fn sse_responses<S>(byte_stream: S) -> impl Stream<Item = Result<ResponsesStreamEvent>>
where
S: Stream<Item = reqwest::Result<Bytes>> + Unpin,
{
let mut buf = BytesMut::new();
let mut byte_stream = byte_stream;
let mut done = false;
futures::stream::poll_fn(move |cx| {
use std::task::Poll;
loop {
if done {
return Poll::Ready(None);
}
if let Some(line) = take_line(&mut buf) {
match parse_responses_frame(&line) {
ResponsesFrame::Skip => continue,
ResponsesFrame::Decode(err) => return Poll::Ready(Some(Err(err))),
ResponsesFrame::Error(err) => {
done = true;
return Poll::Ready(Some(Err(err)));
}
ResponsesFrame::Event(ev) => {
if ev.is_terminal() {
done = true;
}
return Poll::Ready(Some(Ok(ev)));
}
}
}
match byte_stream.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => buf.extend_from_slice(&chunk),
Poll::Ready(Some(Err(e))) => {
done = true;
return Poll::Ready(Some(Err(Error::Http(e))));
}
Poll::Ready(None) => {
if let Some(line) = take_remaining(&mut buf) {
match parse_responses_frame(&line) {
ResponsesFrame::Event(ev) => {
done = true;
return Poll::Ready(Some(Ok(ev)));
}
ResponsesFrame::Error(err) => {
done = true;
return Poll::Ready(Some(Err(err)));
}
ResponsesFrame::Decode(err) => {
done = true;
return Poll::Ready(Some(Err(err)));
}
ResponsesFrame::Skip => {}
}
}
done = true;
return Poll::Ready(None);
}
}
}
})
}
pub(crate) fn ndjson_lines<S>(byte_stream: S) -> impl Stream<Item = Result<BatchResultLine>>
where
S: Stream<Item = reqwest::Result<Bytes>> + Unpin,
{
ndjson::<S, BatchResultLine>(byte_stream)
}
fn ndjson<S, T>(byte_stream: S) -> impl Stream<Item = Result<T>>
where
S: Stream<Item = reqwest::Result<Bytes>> + Unpin,
T: DeserializeOwned,
{
let mut buf = BytesMut::new();
let mut byte_stream = byte_stream;
let mut done = false;
futures::stream::poll_fn(move |cx| {
use std::task::Poll;
loop {
if done {
return Poll::Ready(None);
}
if let Some(line) = take_line(&mut buf) {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
return Poll::Ready(Some(
serde_json::from_str::<T>(trimmed).map_err(Error::from),
));
}
match byte_stream.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => buf.extend_from_slice(&chunk),
Poll::Ready(Some(Err(e))) => {
done = true;
return Poll::Ready(Some(Err(Error::Http(e))));
}
Poll::Ready(None) => {
if let Some(line) = take_remaining(&mut buf) {
let trimmed = line.trim().to_string();
if !trimmed.is_empty() {
done = true;
return Poll::Ready(Some(
serde_json::from_str::<T>(&trimmed).map_err(Error::from),
));
}
}
done = true;
return Poll::Ready(None);
}
}
}
})
}
enum SseLine {
Json(String),
Done,
Skip,
}
fn parse_sse_data_line(line: &str) -> SseLine {
let line = line.trim_end_matches(['\r', '\n']);
let Some(rest) = line.strip_prefix("data:") else {
return SseLine::Skip;
};
let body = rest.trim();
if body.is_empty() {
return SseLine::Skip;
}
if body == DONE {
return SseLine::Done;
}
SseLine::Json(body.to_string())
}
#[allow(clippy::large_enum_variant)]
enum ResponsesFrame {
Event(ResponsesStreamEvent),
Error(Error),
Decode(Error),
Skip,
}
fn parse_responses_frame(line: &str) -> ResponsesFrame {
let line = line.trim_end_matches(['\r', '\n']);
let Some(rest) = line.strip_prefix("data:") else {
return ResponsesFrame::Skip;
};
let body = rest.trim();
if body.is_empty() {
return ResponsesFrame::Skip;
}
let event: ResponsesStreamEvent = match serde_json::from_str(body) {
Ok(ev) => ev,
Err(e) => return ResponsesFrame::Decode(Error::from(e)),
};
if !event.is_recognised() {
return ResponsesFrame::Skip;
}
if event.is_error() {
return ResponsesFrame::Error(Error::Api {
status: 502,
message: event.error_message(),
});
}
ResponsesFrame::Event(event)
}
fn take_line(buf: &mut BytesMut) -> Option<String> {
let pos = buf.iter().position(|&b| b == b'\n')?;
let line = buf.split_to(pos + 1);
let s = String::from_utf8_lossy(&line[..line.len() - 1]).into_owned();
Some(s)
}
fn take_remaining(buf: &mut BytesMut) -> Option<String> {
if buf.is_empty() {
return None;
}
let rest = buf.split();
Some(String::from_utf8_lossy(&rest).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use futures::stream;
fn bytes_stream(parts: Vec<&'static str>) -> impl Stream<Item = reqwest::Result<Bytes>> {
stream::iter(
parts
.into_iter()
.map(|p| Ok(Bytes::from_static(p.as_bytes()))),
)
}
#[tokio::test]
async fn sse_parses_chunks_and_stops_on_done() {
let frames = vec![
"data: {\"id\":\"a\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"He\"}}]}\n",
"\n",
"data: {\"id\":\"a\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"llo\"}}]}\n\n",
"data: [DONE]\n\n",
"data: {\"id\":\"z\"}\n",
];
let s = sse_chunks(bytes_stream(frames));
let out: Vec<_> = s.collect().await;
assert_eq!(out.len(), 2);
let text: String = out
.into_iter()
.map(|c| c.unwrap().first_delta_text().unwrap_or("").to_string())
.collect();
assert_eq!(text, "Hello");
}
#[tokio::test]
async fn sse_handles_split_across_byte_chunks() {
let frames = vec![
"data: {\"id\":\"a\",\"object\":\"chat.completion.chunk\",\"created\":1,\"mod",
"el\":\"m\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"hi\"}}]}\n\ndata: [DONE]\n",
];
let s = sse_chunks(bytes_stream(frames));
let out: Vec<_> = s.collect().await;
assert_eq!(out.len(), 1);
assert_eq!(out[0].as_ref().unwrap().first_delta_text(), Some("hi"));
}
#[tokio::test]
async fn sse_responses_parses_typed_events_and_stops_on_completed() {
let frames = vec![
r#"event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_1","object":"response","status":"in_progress","model":"m"}}
"#,
r#"event: response.output_item.added
data: {"type":"response.output_item.added","sequence_number":1,"output_index":0,"item":{"type":"function_call","call_id":"call_1","name":"get_weather","arguments":""}}
"#,
r#"event: response.function_call_arguments.delta
data: {"type":"response.function_call_arguments.delta","sequence_number":2,"item_id":"fc_1","output_index":0,"delta":"arg-chunk"}
"#,
r#"event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":3,"item_id":"msg_1","output_index":1,"content_index":0,"delta":"Hi"}
"#,
r#"data: {"type":"response.output_text.delta","sequence_number":4,"delta":"!"}
"#,
r#"event: queue.status
data: {"type":"queue.status","sequence_number":5}
"#,
r#"event: response.completed
data: {"type":"response.completed","sequence_number":6,"response":{"id":"resp_1","object":"response","status":"completed","model":"m","store":false,"output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Hi!","annotations":[]}]}],"usage":{"input_tokens":11,"output_tokens":2,"total_tokens":13}}}
"#,
r#"data: {"type":"response.output_text.delta","sequence_number":7,"delta":"ignored"}
"#,
];
let events: Vec<ResponsesStreamEvent> = sse_responses(bytes_stream(frames))
.collect::<Vec<_>>()
.await
.into_iter()
.map(|r| r.unwrap())
.collect();
let kinds: Vec<&str> = events.iter().map(|e| e.kind.as_str()).collect();
assert_eq!(
kinds,
vec![
"response.created",
"response.output_item.added",
"response.function_call_arguments.delta",
"response.output_text.delta",
"response.output_text.delta",
"response.completed",
]
);
let text: String = events.iter().filter_map(|e| e.output_text_delta()).collect();
assert_eq!(text, "Hi!");
let terminal = events.last().unwrap();
assert!(terminal.is_terminal());
let snapshot = terminal.terminal_response().unwrap();
assert_eq!(snapshot.output_text(), "Hi!");
assert_eq!(snapshot.usage.as_ref().unwrap().input_tokens, 11);
assert_eq!(snapshot.store, Some(false));
}
#[tokio::test]
async fn sse_responses_surfaces_error_event_as_api_error() {
let frames = vec![
r#"event: response.created
data: {"type":"response.created","sequence_number":0,"response":{"id":"r","object":"response","status":"in_progress","model":"m"}}
"#,
r#"event: error
data: {"type":"error","sequence_number":1,"code":"server_error","message":"upstream exploded"}
"#,
r#"data: {"type":"response.output_text.delta","sequence_number":2,"delta":"nope"}
"#,
];
let out: Vec<Result<ResponsesStreamEvent>> =
sse_responses(bytes_stream(frames)).collect().await;
assert_eq!(out.len(), 2);
assert_eq!(out[0].as_ref().unwrap().kind, "response.created");
match out[1].as_ref().unwrap_err() {
Error::Api { status, message } => {
assert_eq!(*status, 502);
assert_eq!(message, "upstream exploded");
}
other => panic!("expected Error::Api, got {other:?}"),
}
}
#[tokio::test]
async fn ndjson_parses_lines() {
let lines = vec![
"{\"custom_id\":\"a\",\"response\":{\"status_code\":200,\"body\":{\"id\":\"x\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"m\",\"choices\":[]}}}\n",
"{\"custom_id\":\"b\",\"error\":{\"code\":\"e\",\"message\":\"boom\"}}\n",
];
let s = ndjson_lines(bytes_stream(lines));
let out: Vec<_> = s.collect().await;
assert_eq!(out.len(), 2);
assert_eq!(out[0].as_ref().unwrap().custom_id, "a");
assert_eq!(
out[1].as_ref().unwrap().error.as_ref().unwrap().message,
"boom"
);
}
}