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
}
| Field | Meaningful for |
|---|---|
Text | EventTextDelta, EventThinkingDelta |
ToolCall | EventToolCall |
Usage | EventDone |
StopReason | EventDone |
Raw | varies sharply by adapter — see below |
Caveats
ToolCallandUsageare pointers — always nil-check before dereferencing.EventThinkingDeltais emitted only byprovider/anthropic.EventToolCallis emitted once, when the call's JSON is whole. You never see a fragment.Rawcoverage varies: Anthropic never populates it, the OpenAI-format adapters do so on text deltas only, Gemini on text and tool-call events. The terminalEventDonenever 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
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
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
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.