Skip to main content

Module ndjson

Module ndjson 

Source
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; a CR right 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 LF is 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_SIZE by 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§

NdjsonDecoder
An incremental NDJSON framer: push bytes, take complete lines.
NdjsonStream
The NDJSON lines of a byte stream, such as an HttpResponse body.

Enums§

NdjsonError
Why an NdjsonStream (or an NdjsonDecoder, with E = Infallible) failed.