use super::*;
use futures_util::StreamExt;
fn drive(bytes: &[u8]) -> Vec<ChatStreamEvent> {
let mut dec = SseDecoder::new();
let mut out: Vec<ChatStreamEvent> = dec
.feed(bytes)
.into_iter()
.map(|r| r.expect("no error expected"))
.collect();
if let Some(res) = dec.finish() {
out.push(res.expect("no terminal error expected"));
}
out
}
const HAPPY: &str = "\
data: {\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"\"}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"Hel\"}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"lo\"}}]}\n\n\
data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n\
data: [DONE]\n\n";
#[test]
fn decode_yields_deltas_then_done() {
let events = drive(HAPPY.as_bytes());
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("Hel".into()),
ChatStreamEvent::Delta("lo".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: Some(StopReason::Stop),
usage: Usage::default(),
}),
]
);
}
#[test]
fn decode_handles_split_chunks() {
let mut dec = SseDecoder::new();
let mut events: Vec<ChatStreamEvent> = Vec::new();
for b in HAPPY.as_bytes() {
for r in dec.feed(&[*b]) {
events.push(r.expect("no error"));
}
}
if let Some(res) = dec.finish() {
events.push(res.expect("no terminal error"));
}
assert_eq!(events, drive(HAPPY.as_bytes()));
}
#[test]
fn decode_partial_utf8_across_chunks() {
let frame = "data: {\"choices\":[{\"delta\":{\"content\":\"€\"}}]}\n\n";
let bytes = frame.as_bytes();
for cut in 1..bytes.len() {
let mut dec = SseDecoder::new();
let mut events: Vec<ChatStreamEvent> = Vec::new();
for r in dec.feed(&bytes[..cut]) {
events.push(r.expect("no error"));
}
for r in dec.feed(&bytes[cut..]) {
events.push(r.expect("no error"));
}
assert_eq!(
events,
vec![ChatStreamEvent::Delta("€".into())],
"split at byte {cut} lost the codepoint"
);
}
}
#[test]
fn decode_tolerates_keepalives() {
let stream = "\
: OPENROUTER PROCESSING\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\n\
: ping\n\n\
data: [DONE]\n\n";
let events = drive(stream.as_bytes());
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("hi".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: None,
usage: Usage::default(),
}),
]
);
}
#[test]
fn decode_carries_usage_in_terminal() {
let stream = "\
data: {\"choices\":[{\"delta\":{\"content\":\"x\"}}]}\n\n\
data: {\"choices\":[],\"usage\":{\"prompt_tokens\":11,\"completion_tokens\":3,\"cost\":0.002}}\n\n\
data: [DONE]\n\n";
let events = drive(stream.as_bytes());
let ChatStreamEvent::Done(done) = events.last().expect("has terminal") else {
panic!("last event must be Done, got {:?}", events.last());
};
assert_eq!(done.usage.prompt_tokens, 11);
assert_eq!(done.usage.completion_tokens, 3);
assert_eq!(done.usage.cost_usd, Some(0.002));
}
#[test]
fn decode_surfaces_error_chunk() {
let mut dec = SseDecoder::new();
let results = dec.feed(b"data: {\"error\":{\"message\":\"rate limited\",\"code\":429}}\n\n");
assert_eq!(results.len(), 1);
match &results[0] {
Err(InferenceError::Api { status, body }) => {
assert_eq!(*status, 429);
assert_eq!(body, "rate limited");
}
other => panic!("expected Api error, got {other:?}"),
}
assert!(dec.feed(b"data: [DONE]\n\n").is_empty());
assert!(dec.finish().is_none());
}
#[test]
fn decode_surfaces_string_code_error() {
let mut dec = SseDecoder::new();
let results = dec.feed(
b"data: {\"error\":{\"message\":\"You exceeded your quota\",\"code\":\"insufficient_quota\"}}\n\n",
);
assert_eq!(results.len(), 1);
match &results[0] {
Err(InferenceError::Api { status, body }) => {
assert_eq!(*status, 0, "string code has no numeric status");
assert!(
body.contains("insufficient_quota"),
"code preserved: {body}"
);
assert!(
body.contains("You exceeded your quota"),
"message kept: {body}"
);
}
other => panic!("expected Api error, got {other:?}"),
}
assert!(
dec.finish().is_none(),
"error already terminated the stream"
);
}
#[test]
fn decode_eof_mid_frame_errors() {
let mut dec = SseDecoder::new();
let events = dec.feed(b"data: {\"choices\":[{\"delta\":{\"content\":\"Hel\"}}]}\n\ndata: {\"choices\":[{\"delta\":{\"content\":\"lo");
assert_eq!(
events
.into_iter()
.map(|r| r.expect("ok"))
.collect::<Vec<_>>(),
vec![ChatStreamEvent::Delta("Hel".into())]
);
match dec.finish() {
Some(Err(InferenceError::Transport(msg))) => {
assert!(msg.contains("incomplete"), "message: {msg}");
}
other => panic!("expected incomplete-frame Transport error, got {other:?}"),
}
}
#[test]
fn decode_tolerates_crlf_line_endings() {
let stream = "\
data: {\"choices\":[{\"delta\":{\"content\":\"Hi\"}}]}\r\n\r\n\
data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"stop\"}]}\r\n\r\n\
data: [DONE]\r\n\r\n";
let events = drive(stream.as_bytes());
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("Hi".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: Some(StopReason::Stop),
usage: Usage::default(),
}),
]
);
}
#[test]
fn decode_skips_malformed_frame() {
let stream = "\
data: {not valid json\n\n\
data: {\"choices\":[{\"delta\":{\"content\":\"ok\"}}]}\n\n\
data: [DONE]\n\n";
let events = drive(stream.as_bytes());
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("ok".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: None,
usage: Usage::default(),
}),
]
);
}
#[test]
fn decode_eof_without_done_still_terminates() {
let stream = "data: {\"choices\":[{\"delta\":{\"content\":\"bye\"}},{\"delta\":{}}]}\n\n";
let events = drive(stream.as_bytes());
assert_eq!(events.len(), 2);
assert_eq!(events[0], ChatStreamEvent::Delta("bye".into()));
assert!(matches!(events[1], ChatStreamEvent::Done(_)));
}
#[test]
fn decode_done_then_finish_is_single_terminal() {
let mut dec = SseDecoder::new();
let events = dec.feed(b"data: [DONE]\n\n");
assert_eq!(events.len(), 1);
assert!(matches!(events[0], Ok(ChatStreamEvent::Done(_))));
assert!(dec.finish().is_none());
}
#[test]
fn decode_accumulates_tool_call() {
let stream = "\
data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"loc\\\":\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"SEA\\\"}\"}}]}}]}\n\n\
data: {\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}]}\n\n\
data: [DONE]\n\n";
let events = drive(stream.as_bytes());
let tool_events: Vec<&ToolCallDelta> = events
.iter()
.filter_map(|e| match e {
ChatStreamEvent::ToolCall(d) => Some(d),
_ => None,
})
.collect();
assert_eq!(tool_events.len(), 3);
assert_eq!(tool_events[0].id.as_deref(), Some("call_1"));
assert_eq!(tool_events[0].name.as_deref(), Some("get_weather"));
let args: String = tool_events.iter().map(|d| d.arguments.clone()).collect();
assert_eq!(args, "{\"loc\":\"SEA\"}");
assert!(matches!(
events.last(),
Some(ChatStreamEvent::Done(StreamCompletion {
finish_reason: Some(StopReason::ToolCalls),
..
}))
));
}
#[tokio::test]
async fn decode_event_stream_end_to_end() {
let bytes = HAPPY.as_bytes();
let third = bytes.len() / 3;
let chunks: Vec<Result<Vec<u8>, std::io::Error>> = vec![
Ok(bytes[..third].to_vec()),
Ok(bytes[third..2 * third].to_vec()),
Ok(bytes[2 * third..].to_vec()),
];
let byte_stream = futures_util::stream::iter(chunks);
let events: Vec<ChatStreamEvent> = decode_event_stream(byte_stream)
.map(|r| r.expect("no error"))
.collect()
.await;
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("Hel".into()),
ChatStreamEvent::Delta("lo".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: Some(StopReason::Stop),
usage: Usage::default(),
}),
]
);
}
#[tokio::test]
async fn decode_event_stream_surfaces_transport_error() {
let chunks: Vec<Result<Vec<u8>, std::io::Error>> = vec![
Ok(b"data: {\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\n".to_vec()),
Err(std::io::Error::other("connection reset")),
];
let byte_stream = futures_util::stream::iter(chunks);
let results: Vec<Result<ChatStreamEvent, InferenceError>> =
decode_event_stream(byte_stream).collect().await;
assert_eq!(results.len(), 2);
assert_eq!(
results[0].as_ref().expect("first event ok"),
&ChatStreamEvent::Delta("hi".into())
);
assert!(matches!(results[1], Err(InferenceError::Transport(_))));
}
#[tokio::test]
async fn buffered_stream_replays_response() {
let fixture = r#"{
"id": "gen-1",
"choices": [{"message": {"role": "assistant", "content": "Hello there"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 5, "completion_tokens": 2}
}"#;
let resp: ChatResponse = serde_json::from_str(fixture).expect("deserialise");
let events: Vec<ChatStreamEvent> = buffered_stream(resp)
.map(|r| r.expect("no error"))
.collect()
.await;
assert_eq!(
events,
vec![
ChatStreamEvent::Delta("Hello there".into()),
ChatStreamEvent::Done(StreamCompletion {
finish_reason: Some(StopReason::Stop),
usage: Usage::new(5, 2, 0, 0),
}),
]
);
}