Expand description
Newline-delimited JSON (NDJSON) for streaming adapters.
Some APIs stream one JSON value per line instead of SSE (Ollama’s native
API, several self-hosted servers). NdjsonStream frames a response
Body (or any stream of byte chunks) into its lines, and
NdjsonDecoder is the same framer without the stream, for callers that
feed bytes themselves. The lines drive an adapter’s
ChatFrameDecoder<String> through
the shared FramedChatStream, the
same driver SSE adapters use.
The framing rules:
- A line ends with
LF; aCRright before it belongs to the line ending (CRLF), also when a chunk boundary splits the two. - Blank lines (empty, or only JSON whitespace) are skipped: they carry no value, and servers send them as keep-alives.
- A leading UTF-8 byte order mark is skipped.
- A last line without its
LFis still a line when the body ends, as NDJSON allows. A body cut off in the middle of a line therefore yields a partial line, which the adapter’s JSON decoding rejects. - Each byte is scanned once, however the chunks split the lines, and a
size cap (
NdjsonDecoder::DEFAULT_MAX_LINE_SIZEby default) bounds what a stream can make the framer hold.
Two failures end the stream, as in sse: a line that is
not UTF-8 (NdjsonError::InvalidUtf8: JSON text is UTF-8, and a
replaced byte would change a value silently) and a line past the size cap
(NdjsonError::LineTooLarge). Neither is retryable: asking again
streams the same bytes. NdjsonError::into_error maps them, and a
failed read, to the stream errors the variants name.
Decoding each line’s JSON stays with the adapter: the framer splits, it does not interpret.
use allama_http::ndjson::NdjsonStream;
use futures_util::{FutureExt, StreamExt, stream};
// A `CRLF` split by a chunk boundary, a blank keep-alive line, and a last
// line without its line ending.
let body = stream::iter([
Ok::<_, std::io::Error>(&b"{\"text\":\"Hel\"}\r"[..]),
Ok(&b"\n\n{\"text\":\"lo\"}\n{\"done\":true}"[..]),
]);
let lines: Vec<_> = NdjsonStream::new(body).collect().now_or_never().unwrap();
let lines: Vec<_> = lines.into_iter().map(Result::unwrap).collect();
assert_eq!(lines, [r#"{"text":"Hel"}"#, r#"{"text":"lo"}"#, r#"{"done":true}"#]);Structs§
- Ndjson
Decoder - An incremental NDJSON framer: push bytes, take complete lines.
- Ndjson
Stream - The NDJSON lines of a byte stream, such as an
HttpResponsebody.
Enums§
- Ndjson
Error - Why an
NdjsonStream(or anNdjsonDecoder, withE=Infallible) failed.