Skip to content
skyl

StreamEvent

One incremental update from a streaming response.

StreamEvent is a tagged union: Type says which fields are meaningful and the rest are zero. Switch on Type.

Reference#

type StreamEvent struct { Type EventType Text string ToolCall *ToolCall Usage *Usage StopReason StopReason Raw json.RawMessage }
FieldMeaningful for
TextEventTextDelta, EventThinkingDelta
ToolCallEventToolCall
UsageEventDone
StopReasonEventDone
Rawvaries sharply by adapter — see below

Caveats

  • ToolCall and Usage are pointers — always nil-check before dereferencing.
  • EventThinkingDelta is emitted only by provider/anthropic.
  • EventToolCall is emitted once, when the call's JSON is whole. You never see a fragment.
  • Raw coverage varies: Anthropic never populates it, the OpenAI-format adapters do so on text deltas only, Gemini on text and tool-call events. The terminal EventDone never carries it on any adapter.
  • Unparseable SSE frames are skipped rather than fatal, because providers interleave keep-alives and vendor-specific records.

Usage#

Handling every type

goCompiles
for stream.Next() {
	switch ev := stream.Event(); ev.Type {
	case skyl.EventTextDelta:
		fmt.Print(ev.Text)
	case skyl.EventThinkingDelta:
		fmt.Fprint(os.Stderr, ev.Text) // reasoning, not the answer
	case skyl.EventToolCall:
		if ev.ToolCall != nil {
			calls = append(calls, *ev.ToolCall)
		}
	case skyl.EventDone:
		if ev.Usage != nil {
			total = total.Add(*ev.Usage)
		}
		stop = ev.StopReason
	}
}
for stream.Next() {
	switch ev := stream.Event(); ev.Type {
	case skyl.EventTextDelta:
		fmt.Print(ev.Text)
	case skyl.EventThinkingDelta:
		fmt.Fprint(os.Stderr, ev.Text) // reasoning, not the answer
	case skyl.EventToolCall:
		if ev.ToolCall != nil {
			calls = append(calls, *ev.ToolCall)
		}
	case skyl.EventDone:
		if ev.Usage != nil {
			total = total.Add(*ev.Usage)
		}
		stop = ev.StopReason
	}
}

Showing a thinking pane only where it exists

goCompiles
var sawThinking bool
for stream.Next() {
	if ev := stream.Event(); ev.Type == skyl.EventThinkingDelta {
		if !sawThinking {
			ui.OpenThinkingPane() // lazily, so three adapters show no empty box
			sawThinking = true
		}
		ui.AppendThinking(ev.Text)
	}
}
var sawThinking bool
for stream.Next() {
	if ev := stream.Event(); ev.Type == skyl.EventThinkingDelta {
		if !sawThinking {
			ui.OpenThinkingPane() // lazily, so three adapters show no empty box
			sawThinking = true
		}
		ui.AppendThinking(ev.Text)
	}
}

Collecting raw frames

goCompiles
var frames []json.RawMessage
for stream.Next() {
	if ev := stream.Event(); len(ev.Raw) > 0 {
		frames = append(frames, ev.Raw)
	}
}
var frames []json.RawMessage
for stream.Next() {
	if ev := stream.Event(); len(ev.Raw) > 0 {
		frames = append(frames, ev.Raw)
	}
}

Troubleshooting#

I never receive EventThinkingDelta

Only Anthropic emits it. The others do reason — their streaming formats simply do not disclose it as a distinct frame.

EventDone reported nil Usage

On OpenAI-family hosts, usage requires stream_options.include_usage to be honoured; many compatible hosts ignore it.

Raw is empty on Anthropic

It is never populated there. If you need raw fidelity on Anthropic, use non-streaming Complete, where Response.Raw is always present.

Edit this page on GitHub