NAME
Langertha::Skeid::Protocol::Ollama::Stream - Rewrites an OpenAI SSE stream as Ollama newline-delimited JSON
VERSION
version 0.003
DESCRIPTION
The counterpart to Langertha::Skeid::Protocol::Anthropic::Stream for Ollama's /api/chat, and a simpler job: Ollama streams newline-delimited JSON objects rather than SSE events, each one a whole message with a done flag. There is no prologue and no event framing — just one line per delta and a final line that says it is over.
my $stream = Langertha::Skeid::Protocol::Ollama::Stream->new(model => 'qwen3');
$write->($stream->start); # empty, by design
$write->($stream->delta($chunk)) for @chunks;
$write->($stream->finish);
The trailing line matters more than it looks: an Ollama client reads token counts from it and treats the stream as unfinished without it.
/api/generate streams the same lines with the text under response instead of message; shape => 'generate' selects that, the default chat is /api/chat.
my $stream = Langertha::Skeid::Protocol::Ollama::Stream->new(model => 'qwen3', shape => 'generate');
new
my $stream = Langertha::Skeid::Protocol::Ollama::Stream->new(model => $requested_model,
shape => 'chat');
model is named on every line until an upstream chunk names its own; shape is chat (default) or generate. Tool calls are only rendered for chat.
content_type
application/x-ndjson. Not SSE: relaying the upstream's text/event-stream here would tell an Ollama client to parse something it does not speak.
start
Nothing. Ollama has no prologue — the first line a client sees is the first delta. Present so both stream translators answer the same three calls.
delta
One decoded OpenAI chunk becomes one Ollama line when it carries text or completed tool calls, or nothing for a role-only chunk or the final usage-only one. OpenAI tool-call fragments are fed to "parse_stream_chunk" in Langertha::Engine::OpenAIBase with state private to this stream; only its completed Langertha::ToolCall objects are rendered, through to_ollama. Usage and finish reason are recorded for the closing line. A top-level OpenAI error object ends the stream with "error_event"; a parser rejection does the same with a generic message rather than reflecting parser/provider details.
finish
The closing line: done true, the reason, and the token counts an Ollama client reads its statistics from. done_reason => ..., input_tokens => ... and output_tokens => ... override what the stream recorded. Idempotent. A pending tool call whose stream supplied no final chunk is an "error_event", not a successful close; failures while building the closing line are contained at the same boundary.
error_event
my $bytes = $stream->error_event(500, 'Upstream error: ...');
Ends the stream with Ollama's error line, {"error":"<message"}> -- how Ollama itself reports a failure after the stream has opened, and what its clients check every line for. The status is accepted for the same call shape as "error_event" in Langertha::Skeid::Protocol::Anthropic::Stream and is not on the wire: the HTTP status went out with the first line.
The stream is finished afterwards: delta and finish return nothing, so no done: true line follows and a client cannot mistake a failed stream for a complete one. Returns nothing if the stream has already finished.
errored
True once "error_event" ended the stream, so the proxy records the request as failed even though the HTTP status was 200.
usage
my ($input, $output, $content_bytes) = $stream->usage;
What the stream carried, for the usage event. content_bytes counts UTF-8 bytes of the text this translator wrote and becomes the event's content_bytes, recorded beside the token counts on every stream -- an observation, never an estimate of tokens.
SEE ALSO
Langertha::Skeid::Protocol::Ollama, Langertha::Skeid::Protocol::Anthropic::Stream
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-skeid/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.