Claudio.Messages.Stream (Claudio v0.7.0)
View SourceUtilities for parsing and consuming Server-Sent Events (SSE) from streaming Messages API responses.
parse_events/1 emits [:claudio, :messages, :stream, :start | :stop] around each consumption
(and the older [:claudio, :messages, :stream, :usage]); see the telemetry guide.
Event Types
The Messages API streaming responses include the following event types:
message_start- Initial message with empty contentcontent_block_start- Beginning of a content blockcontent_block_delta- Incremental content updates (text, JSON, thinking); read them withaccumulate_text/1/accumulate_thinking/1content_block_stop- End of a content blockmessage_delta- Top-level message changes (usage updates)message_stop- Stream completionping- Keep-alive eventserror- Error events
Example
The simplest way to stream is to_response/2: print text as it arrives and still get
usage, stop_reason and tool calls at the end.
{:ok, stream_response} =
Claudio.Messages.create(client, Claudio.Messages.Request.enable_streaming(request))
{:ok, response} =
Claudio.Messages.Stream.to_response(stream_response, on_text: &IO.write/1)For lower-level control, enumerate the parsed events:
stream_response
|> Claudio.Messages.Stream.parse_events()
|> Stream.filter(&match?({:ok, %{event: "content_block_delta"}}, &1))
|> Enum.each(fn {:ok, event} ->
IO.puts(event.data["delta"]["text"])
end)Consume once
A streaming body can be read once, and only by the process that called
Claudio.Messages.create/2: a second read waits forever, and a read from another
process raises. Use the :on_text / :on_event options of to_response/2 instead of
enumerating the response twice.
Summary
Functions
Emits the text of each text_delta event as a stream of text chunks.
Emits {block_index, text} for every thinking_delta with non-empty text.
Accumulates all events and returns the final complete message.
Filters stream to only specific event types.
Parses Server-Sent Events from a streaming response body.
Consumes a streaming response in one pass and returns the final Claudio.Messages.Response.
Types
Functions
@spec accumulate_text(Enumerable.t()) :: Enumerable.t()
Emits the text of each text_delta event as a stream of text chunks.
Enumerate them, or Enum.join/1 them for the full text.
Example
response
|> Stream.parse_events()
|> Stream.accumulate_text()
|> Enum.each(&IO.write/1)
@spec accumulate_thinking(Enumerable.t()) :: Enumerable.t()
Emits {block_index, text} for every thinking_delta with non-empty text.
The index tells one thinking block from the next: with display: :updates each
block is a separate progress note, and a block's first emission is the point the
API docs say to treat it as an update. Empty deltas (display: :omitted),
other deltas, other events and {:error, _} items emit nothing.
The index is nil only for a malformed frame with no "index" (the API always
sends one); its text is still emitted rather than dropped.
Example
response
|> Stream.parse_events()
|> Stream.accumulate_thinking()
|> Enum.each(fn {index, text} -> IO.puts("[#{index}] #{text}") end)
@spec build_final_message(Enumerable.t()) :: {:ok, map()} | {:error, term()}
Accumulates all events and returns the final complete message.
Streamed tool input (input_json_delta chunks on tool_use, server_tool_use and
mcp_tool_use blocks) is decoded into the block's "input". If a block's accumulated
JSON is invalid — typically output cut off by max_tokens mid-call — the result is
{:error, {:invalid_tool_input_json, index, partial_json}}.
Example
{:ok, final_message} =
response
|> Stream.parse_events()
|> Stream.build_final_message()
@spec filter_events(Enumerable.t(), [String.t()]) :: Enumerable.t()
Filters stream to only specific event types.
Example
response
|> Stream.parse_events()
|> Stream.filter_events(["content_block_delta", "message_stop"])
@spec parse_events(Req.Response.t() | Enumerable.t()) :: Enumerable.t()
Parses Server-Sent Events from a streaming response body.
Returns a Stream of {:ok, event} or {:error, reason} tuples.
Pass the whole %Req.Response{} from Claudio.Messages.create/2 to link the stream span
to the create span; passing response.body still works, unlinked.
Emits [:claudio, :messages, :stream, :start | :stop] around each consumption of the
returned stream; see the telemetry guide.
Example
response
|> Stream.parse_events()
|> Enum.to_list()
@spec to_response( Req.Response.t() | Enumerable.t(), keyword() ) :: {:ok, Claudio.Messages.Response.t()} | {:error, term()}
Consumes a streaming response in one pass and returns the final Claudio.Messages.Response.
The simplest way to stream: print text as it arrives and still get usage,
stop_reason and tool calls at the end.
{:ok, response} =
Claudio.Messages.Stream.to_response(stream_response, on_text: &IO.write/1)
response.usage.output_tokensOptions
:on_text— called with each text delta, in order.:on_event— called with each parsed event ({:ok, event} | {:error, reason}).
A streaming body can be read once, and only by the process that called
Claudio.Messages.create/2: a second read waits forever, and a read from another
process raises. Use :on_text / :on_event instead of enumerating the response twice.
An SSE error event is returned as {:error, %Claudio.APIError{}}; other failures are
as for build_final_message/1. That error's status_code is 200 (the stream's HTTP
status), so match on its type.