streaming

package
v0.1.93 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultFinishReason = "content_filter"

DefaultFinishReason is the chat finish_reason used when a stream is cut without an explicit one.

View Source
const MaxMinChunkChars = 16 * 1024

MaxMinChunkChars caps TransformOptions.MinChunkChars: a policy cannot ask for more than this many characters of a choice's text to be collected before the transformer sees it. The run that reaches the threshold is presented whole, so a single large delta can carry more.

Variables

View Source
var ErrBufferLimit = errors.New("streaming: buffered stream exceeded size limit")

ErrBufferLimit reports that a buffered upstream exceeded MaxBytes.

View Source
var ErrEventTooLarge = errors.New("streaming: event exceeded the inspectable size")

ErrEventTooLarge reports that an upstream event exceeded MaxEventBytes.

View Source
var ErrNoEvents = errors.New("streaming: no events to assemble")

ErrNoEvents is returned when a response cannot be assembled because the stream carried no decodable payload.

View Source
var ErrNotTextEvent = errors.New("streaming: event carries no rewritable text")

ErrNotTextEvent is returned by RewriteText for events without delta text.

View Source
var ErrStreamClosed = errors.New("streaming: stream closed")

ErrStreamClosed is returned by Read after Close.

View Source
var ErrStreamIncomplete = errors.New("provider stream ended before completion")

ErrStreamIncomplete marks a provider stream that stopped before its terminal event. Errors carrying it came from reading the provider, never from writing to the client, so they must not be classified as a client disconnect even when they wrap a connection reset.

Functions

func AssembleChatResponse added in v0.1.90

func AssembleChatResponse(events []Event) (*core.ChatResponse, error)

AssembleChatResponse rebuilds a chat completion from the chunks of a chat stream: text, reasoning and tool call deltas are concatenated per choice, finish_reason and usage are taken from the chunks carrying them, and the envelope (id, model, created, system_fingerprint, provider) from the first chunk that has each member.

func AssembleResponsesResponse added in v0.1.90

func AssembleResponsesResponse(events []Event) (*core.ResponsesResponse, error)

AssembleResponsesResponse rebuilds a Responses API response from its stream. The response object carried by response.completed, incomplete or failed wins; without one, output items are rebuilt from output_item and delta events (text deltas without an item become one message item) and the status is "incomplete".

func IncompleteStreamError added in v0.1.93

func IncompleteStreamError(err error) error

IncompleteStreamError wraps the read failure that ended a provider stream early; a clean close is reported as io.ErrUnexpectedEOF. Both ErrStreamIncomplete and the underlying error stay visible to errors.Is.

func NewBufferedSSEStream added in v0.1.90

func NewBufferedSSEStream(ctx context.Context, upstream io.ReadCloser, codec Codec, finish Finisher, opts BufferOptions) io.ReadCloser

NewBufferedSSEStream holds the whole upstream before anything reaches the client. The first Read starts draining upstream into a bounded buffer; while draining, Read blocks and returns a keep-alive comment whenever the interval elapses. Once upstream ends the finisher runs and its replay is served until EOF. Cancelling ctx (client gone) stops the drain, closes upstream and makes Read return ctx.Err() without running the finisher. When upstream fails mid-stream the finisher still runs on what arrived and the upstream error is returned once the replay has been served.

Closing upstream must unblock a pending Read (net/http bodies do).

func NewObservedSSEStream

func NewObservedSSEStream(stream io.ReadCloser, observers ...Observer) io.ReadCloser

NewObservedSSEStream returns the original stream when there are no observers.

func NewSlowdownStream added in v0.1.74

func NewSlowdownStream(ctx context.Context, source io.ReadCloser, factor float64, inferenceStarted time.Time) io.ReadCloser

NewSlowdownStream drains source in the background and releases each read chunk on a scaled timeline. factor is extra time: 0.5 makes a chunk that arrived 2s after inference start visible at 3s. Draining independently lets the upstream continue producing while delayed chunks accumulate in memory.

func NewTransformedSSEStream added in v0.1.90

func NewTransformedSSEStream(upstream io.ReadCloser, codec Codec, t Transformer, opts TransformOptions) io.ReadCloser

NewTransformedSSEStream relays upstream through t. Reads are pull-based: each Read consumes upstream bytes, splits them into SSE events, calls t and returns the resulting bytes. Events t passes are relayed verbatim; comments and unparseable blocks are relayed without consulting t.

A decision to terminate, an error from t, a Termination from OnEnd, or an event larger than MaxEventBytes ends the stream with the codec's terminal events (fail-closed with error code "plugin_failure" for errors and "event_too_large" for oversized events), closes upstream, and makes later Reads return io.EOF.

Lookbehind re-segmentation (LookbehindChars = N > 0) applies to text deltas and to tool-call argument deltas, each kind in its own window: a choice's text is one window and each of its tool calls' arguments (Event.Call) another, with a withheld tail of at most N characters (runes), initially empty:

  1. When a delta arrives, t sees one event of its kind whose Text is the window tail+delta (Event.Overlap is the tail's length). Its decision applies to the whole window: pass keeps it, replace substitutes Decision.Text for it, drop discards it (tail included).
  2. Of the resulting window, everything but the last N characters is emitted to the client; the last N become the new tail.
  3. An event that is not held (reasoning, finish, usage, other, a tool call announced with empty arguments) first flushes every window, a delta of another kind for the same choice flushes that choice's windows of other kinds (its text before its first tool call), and the upstream end flushes them all before OnEnd: t sees the tail once more (Overlap equal to its length) and the result is emitted in full. Windows of one kind (parallel tool calls) are independent and may interleave without flushing each other.

The first chunk of a window's run stays the template of its re-segmented events until something is emitted from it, so members only that chunk carries (a tool call's id and name sent with its first arguments) reach the client once.

Consequently a pattern of up to N+1 characters is always visible to t in one event before any of its characters reaches the client, at the cost of N characters of delay.

Coalescing (MinChunkChars = M > 0) collects the text deltas of a choice until at least M new characters are pending and only then runs step 1 on the window tail+pending, so t sees runs of at least M characters (the final run at a flush may be shorter). Both work together: the tail is what t already saw, the pending text is new, and Event.Overlap still counts the tail. Re-segmented events are rendered with RewriteText from the most recent raw chunk of that choice, so every other member of that chunk is preserved (a Responses event keeps its sequence_number). Members that must arrive once per choice (a chat chunk's finish_reason and usage, see Codec.StripTerminal) are left off the emitted head and travel with the chunk's withheld tail, so they follow the chunk's last text; when that text is dropped or emptied they go out on a chunk with empty text, so the stream still ends well-formed.

func SynthesizeChatStream added in v0.1.90

func SynthesizeChatStream(resp *core.ChatResponse, includeUsage bool) []byte

SynthesizeChatStream renders a chat completion as a chat SSE stream: per choice a role chunk, a reasoning chunk (when the message carries reasoning_content), a content chunk, one chunk per tool call and a finish chunk; then a usage chunk when includeUsage is set, and [DONE].

func SynthesizeResponsesStream added in v0.1.90

func SynthesizeResponsesStream(resp *core.ResponsesResponse) []byte

SynthesizeResponsesStream renders a Responses API response as an event stream: response.created, response.in_progress, then per output item the added/delta/done events (whole text as one output_text.delta), the terminal event matching resp.Status, and [DONE]. Events carry sequence_number.

Types

type Action added in v0.1.90

type Action string

Action is what a Transformer wants done with an event.

const (
	ActionPass      Action = "pass"
	ActionDrop      Action = "drop"
	ActionReplace   Action = "replace"
	ActionTerminate Action = "terminate"
)

type BufferOptions added in v0.1.90

type BufferOptions struct {
	// MaxBytes caps the buffered upstream bytes; 0 selects 4 MiB. Exceeding
	// it fails closed with error code "response_too_large".
	MaxBytes int
	// KeepAliveInterval spaces the SSE comments sent to the client while the
	// upstream is being drained; 0 selects 15s, a negative value disables
	// them.
	KeepAliveInterval time.Duration
	// KeepAliveComment is the comment text; default ": gomodel-buffering".
	KeepAliveComment string
	// OnError receives the buffer limit and finisher errors behind a
	// fail-closed replay.
	OnError func(error)
}

BufferOptions tunes NewBufferedSSEStream.

type Codec added in v0.1.90

type Codec interface {
	// Decode classifies raw; anything not understood is KindOther. Data in
	// the returned event aliases raw.Data.
	Decode(raw RawEvent, seq int) Event
	// Track records ev as emitted to the client. Streams call it for every
	// decoded event they relay, passing the rewritten event when the text
	// was changed. A stream that emits nothing before its terminal events
	// (buffering) never calls it.
	Track(ev Event)
	// RewriteText returns a copy of ev whose delta text is replaced with
	// text; every other member of the payload is preserved.
	RewriteText(ev Event, text string) (Event, error)
	// Terminate renders the final bytes that end a cut stream, [DONE]
	// included.
	Terminate(t Termination) [][]byte
	// StripTerminal returns a copy of a text event without the members that
	// must reach the client once per choice (a chat chunk's finish_reason
	// and usage); ok reports that ev carried any. Lookbehind
	// re-segmentation emits the head of a chunk from the stripped copy and
	// the withheld tail from the original, so those members arrive once,
	// with the chunk's last text.
	StripTerminal(ev Event) (Event, bool)
	// Split divides a raw event that carries several choices into one raw
	// event per choice, so each is decoded and transformed on its own. It
	// returns nil when raw needs no splitting.
	Split(raw RawEvent) []RawEvent
	// Restate rewrites an event that repeats text already streamed (the
	// Responses *.done and response.completed events) so it carries the
	// text that was actually emitted after transformation; ok reports that
	// ev was changed. Codecs without such events return ev, false.
	Restate(ev Event) (Event, bool)
}

Codec understands one stream dialect: it classifies raw events, rewrites delta text, and renders the events that end a cut stream. Codecs are stateful and must be used for a single stream: Decode remembers envelope facts (ids, models, timestamps, sequence numbers) and Track remembers what has reached the client (open output items, text emitted so far, finished choices) so Terminate can close the stream consistently.

func ChatCodec added in v0.1.90

func ChatCodec() Codec

ChatCodec returns a codec for OpenAI chat.completion.chunk streams.

func ResponsesCodec added in v0.1.90

func ResponsesCodec() Codec

ResponsesCodec returns a codec for Responses API event streams.

type Decision added in v0.1.90

type Decision struct {
	Action Action
	// Text is the replacement delta text for ActionReplace. Text,
	// reasoning, and tool-call argument deltas can be replaced.
	Text string
	// Terminate describes how to end the stream for ActionTerminate; nil
	// selects the codec defaults (finish_reason "content_filter").
	Terminate *Termination
}

Decision is a Transformer's verdict on one event.

type Event added in v0.1.90

type Event struct {
	// Seq is the zero-based position of the event in the stream, counting
	// decoded events only (comments and blank blocks are not numbered).
	Seq  int
	Kind EventKind
	// Choice is the chat choice index; always 0 for Responses streams.
	Choice int
	// Call is the index of the tool call a tool-call delta belongs to: the
	// delta's tool_calls[].index in a chat stream, the output_index of the
	// function_call item in a Responses stream. 0 for other kinds.
	Call int
	// Text is the delta text for text and reasoning deltas, and the arguments
	// fragment carried by a tool call delta. Empty for other kinds.
	Text string
	// Overlap is the number of leading characters (runes) of Text that were
	// already shown in the previous event of this window (a choice's text,
	// or one of its tool calls' arguments). It is non-zero only for deltas
	// re-segmented under lookbehind, where consecutive windows overlap;
	// Text[Overlap:] is the new text.
	Overlap int
	// Final marks the last event of a window: its text is emitted in full
	// after the decision (the stream ended, or a delta of another kind
	// flushed the window), so nothing of it is withheld for a next event.
	Final bool
	// ClosesChoice marks a delta event whose chunk also carries the
	// finish_reason of its choice (a chat stream may end text and finish in
	// one chunk), so once it is emitted the choice needs no finish chunk
	// from Terminate. Cleared on a copy stripped of that member.
	ClosesChoice bool
	// Name is the SSE "event:" field. Empty for chat chunks.
	Name string
	// Data is the JSON payload, or the literal [DONE]. For events handed to a
	// Transformer it is only valid during the call; copy it to retain it.
	Data []byte
}

Event is one decoded SSE event in a canonical (chat or Responses) stream.

func (*Event) Encode added in v0.1.90

func (e *Event) Encode() []byte

Encode renders the event as SSE: "event: <name>\n" when Name is set, followed by one "data:" line per line of Data and a blank line.

type EventFilter

type EventFilter interface {
	WantsJSONEvent(raw []byte) bool
}

EventFilter is an optional Observer extension. Observers that consume only specific payloads can report disinterest from the raw event bytes; when no observer wants an event, the stream skips JSON decoding entirely. Filters must under-approximate disinterest only: an observer may still receive events it did not ask for when another observer wants them.

type EventKind added in v0.1.90

type EventKind string

EventKind classifies a decoded SSE event for stream transformers.

const (
	KindTextDelta      EventKind = "text_delta"
	KindToolCallDelta  EventKind = "tool_call_delta"
	KindReasoningDelta EventKind = "reasoning_delta"
	KindFinish         EventKind = "finish"
	KindUsage          EventKind = "usage"
	KindOther          EventKind = "other"
	// KindDone is the "data: [DONE]" sentinel that ends OpenAI-style streams.
	KindDone EventKind = "done"
)

type EventScanner added in v0.1.90

type EventScanner struct {
	// MaxEventBytes bounds one event's body. A larger event is relayed as
	// Oversized fragments and never parsed, whether it arrived complete in
	// one chunk or is still being buffered across chunks, so the limit does
	// not depend on upstream read boundaries. Zero selects 256 KiB.
	MaxEventBytes int
	// contains filtered or unexported fields
}

EventScanner incrementally splits an SSE byte stream into events.

Feed returns the events completed by the chunk; Flush returns the trailing partial block, if any, once the stream has ended. Slices in returned events alias the scanner's buffer or the fed chunk and are valid only until the next Feed or Flush call.

func (*EventScanner) Feed added in v0.1.90

func (s *EventScanner) Feed(chunk []byte) []RawEvent

Feed consumes the next chunk of the stream and returns the completed events.

func (*EventScanner) Flush added in v0.1.90

func (s *EventScanner) Flush() []RawEvent

Flush returns the unterminated trailing block, if any, and resets the scanner. Call it once the upstream has ended.

type Finisher added in v0.1.90

type Finisher func(events []Event, raw []byte) (replay []byte, err error)

Finisher receives the whole upstream once drained: its decoded events (comments and oversized fragments excluded, [DONE] included) and its raw bytes. It returns the bytes to replay to the client; nil replays raw unchanged. An error fails closed with error code "plugin_failure".

type ObservedSSEStream

type ObservedSSEStream struct {
	io.ReadCloser
	// contains filtered or unexported fields
}

ObservedSSEStream proxies bytes unchanged while parsing SSE JSON events once and fanning them out to observers.

func (*ObservedSSEStream) Close

func (s *ObservedSSEStream) Close() error

func (*ObservedSSEStream) Read

func (s *ObservedSSEStream) Read(p []byte) (n int, err error)

type Observer

type Observer interface {
	OnJSONEvent(payload map[string]any)
	OnStreamClose()
}

Observer receives parsed JSON SSE payloads in stream order. Implementations must treat the payload as read-only.

type RawEvent added in v0.1.90

type RawEvent struct {
	// Name is the "event:" field, if any.
	Name string
	// Data joins the block's "data:" lines with "\n" (per the SSE spec). Nil
	// when the block carries no data field.
	Data []byte
	// Comment marks a block without a data field (a ":" comment, an id/retry
	// only block, or an empty block). Such blocks are relayed verbatim.
	Comment bool
	// Oversized marks a fragment of an event that exceeded MaxEventBytes; the
	// fragment is relayed unparsed and never decoded.
	Oversized bool
	// Raw holds the block's original bytes including its terminating blank
	// line (when one was seen), so pass-through can be byte-identical.
	Raw []byte
}

RawEvent is one SSE block split off by EventScanner.

type Renumberer added in v0.1.92

type Renumberer interface {
	// BeginRenumber puts the codec in renumbering mode: from then on the
	// codec assigns the outgoing numbers, both to the events handed to
	// Renumber and to the ones Terminate renders.
	BeginRenumber()
	// Renumber returns a copy of ev numbered with the next outgoing number;
	// ok reports that ev was changed, and is false when the event carries no
	// number or already carries the right one.
	Renumber(ev Event) (Event, bool)
}

Renumberer is implemented by codecs whose dialect numbers the events of a stream (the Responses API sequence_number, which must run 0..N without gaps). A stream that may drop, merge, split or inject events renumbers the ones it delivers so the client still sees a contiguous sequence.

type StallReporter added in v0.1.91

type StallReporter interface {
	StallError() error
}

StallReporter is implemented by the server's stall deadline writer, which sits beneath the response wrappers whose flush methods return no error. It is looked up through the Unwrap chain of whatever writer a handler holds, and asked after a flush whether the client stopped reading.

func FindStallReporter added in v0.1.91

func FindStallReporter(w any) StallReporter

FindStallReporter walks the Unwrap chain from w down to the stall writer, or returns nil when the route runs without one.

type StreamBuffer

type StreamBuffer struct {
	// contains filtered or unexported fields
}

StreamBuffer is a non-concurrent FIFO byte buffer for short-lived stream converters. It must not be copied after first use.

func NewStreamBuffer

func NewStreamBuffer(initialCapacity int) StreamBuffer

func (*StreamBuffer) AppendBytes

func (b *StreamBuffer) AppendBytes(data []byte)

func (*StreamBuffer) AppendString

func (b *StreamBuffer) AppendString(data string)

func (*StreamBuffer) Consume

func (b *StreamBuffer) Consume(n int)

func (*StreamBuffer) Len

func (b *StreamBuffer) Len() int

func (*StreamBuffer) Read

func (b *StreamBuffer) Read(p []byte) int

func (*StreamBuffer) Release

func (b *StreamBuffer) Release()

Release returns the buffer's storage to the pool. No slice derived from the buffer (Unread, or bytes handed to a decoder that may alias its input) may be retained past this call: the storage is immediately reusable by another stream and retained views would see another request's data.

func (*StreamBuffer) Unread

func (b *StreamBuffer) Unread() []byte

type Termination added in v0.1.90

type Termination struct {
	// FinishReason is the chat finish_reason (default "content_filter"). For
	// Responses it selects incomplete_details.reason.
	FinishReason string
	// ErrorCode, when set, ends the stream with an error (chat: an error
	// payload before the finish chunk; Responses: response.failed).
	ErrorCode    string
	ErrorMessage string
	// Text is optional final text emitted as one more delta before finishing,
	// for example a canned safe message.
	Text string
	// Usage, when set, is the provider usage object rendered into the
	// terminal event (chat: the finish chunk; Responses: response.usage), so
	// accounting observers downstream still see the tokens a cut stream
	// consumed.
	Usage any
}

Termination describes how a cut stream ends.

type TransformOptions added in v0.1.90

type TransformOptions struct {
	// LookbehindChars withholds this many trailing characters of text per
	// choice so a pattern that spans two chunks is visible to the transformer
	// in one event. 0 disables re-segmentation. See NewTransformedSSEStream.
	LookbehindChars int
	// MinChunkChars collects the text deltas of a choice until at least this
	// many new characters (runes) are pending and presents them to the
	// transformer as one text event. 0 presents deltas as they arrive; values
	// above MaxMinChunkChars are clamped to it. See NewTransformedSSEStream.
	MinChunkChars int
	// MaxEventBytes bounds one SSE event. A larger event cannot be inspected
	// in flight, so the stream ends fail-closed with error code
	// "event_too_large" instead of relaying it past the transformer. 0
	// selects 4 MiB.
	MaxEventBytes int
	// OnError receives non-fatal problems (a replace on a non-text event, a
	// failed rewrite) and the error behind a fail-closed termination.
	OnError func(error)
}

TransformOptions tunes NewTransformedSSEStream.

type Transformer added in v0.1.90

type Transformer interface {
	// OnEvent is called for every decoded event except the [DONE] sentinel.
	// The event, including Data, is only valid during the call.
	OnEvent(ev *Event) (Decision, error)
	// OnEnd is called once after the last upstream event and before [DONE]
	// (or at upstream EOF when no [DONE] arrives). Returning a Termination
	// cuts the stream there.
	OnEnd() (*Termination, error)
}

Transformer inspects and edits a stream event by event.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL